@sealant/mend 0.36.0-next.663 → 0.36.0-next.675
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VOP6USOZ.js → chunk-TP4A72RF.js} +43 -2
- package/dist/{dashboard-CEJN6IOO.js → dashboard-RAZDVFR7.js} +8 -2
- package/dist/main.js +1521 -175
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect-pi.1 +1 -1
- package/man/mend-connect.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-repo.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-folder-create.1 +1 -1
- package/man/mend-folder-list.1 +1 -1
- package/man/mend-folder-push.1 +1 -1
- package/man/mend-folder-rm.1 +1 -1
- package/man/mend-git-author.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-logs.1 +13 -2
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-memory-import.1 +1 -1
- package/man/mend-memory-rm.1 +1 -1
- package/man/mend-memory-show.1 +1 -1
- package/man/mend-memory.1 +1 -1
- package/man/mend-models.1 +1 -1
- package/man/mend-operator-exposure.1 +2 -2
- package/man/mend-operator-gate.1 +1 -1
- package/man/mend-operator-grant-owner.1 +1 -1
- package/man/mend-operator-org-create.1 +1 -1
- package/man/mend-operator-org-invite-owner.1 +1 -1
- package/man/mend-operator-org-list.1 +1 -1
- package/man/mend-operator-org-rename.1 +1 -1
- package/man/mend-operator-reset-link.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-pull.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +2 -2
- package/man/mend-secrets-add.1 +1 -1
- package/man/mend-secrets-rm.1 +1 -1
- package/man/mend-secrets.1 +1 -1
- package/man/mend-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +42 -2
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +4 -2
- package/man/mend-server-stop.1 +2 -2
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +10 -4
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +13 -6
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +1 -1
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-wait.1 +5 -5
- package/man/mend-workspace-replace.1 +1 -1
- package/man/mend-worktrees-rm.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +3 -3
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
landingFactsFromWire,
|
|
41
41
|
launcherNameOf,
|
|
42
42
|
matchProjectByCwd,
|
|
43
|
+
mayStillBeWorking,
|
|
43
44
|
narrowCredential,
|
|
44
45
|
noAnswerError,
|
|
45
46
|
normalizeGitAuthor,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
replaceConfirmLines,
|
|
52
53
|
repositoryCloneUrlIssue,
|
|
53
54
|
sameGrant,
|
|
55
|
+
serviceStartCorrelation,
|
|
54
56
|
servicesHoldOf,
|
|
55
57
|
sessionDisplayName,
|
|
56
58
|
sessionWorkspaceLines,
|
|
@@ -68,10 +70,11 @@ import {
|
|
|
68
70
|
workspaceRetirementLine,
|
|
69
71
|
worktreeJoinLine,
|
|
70
72
|
worktreeListsPeople
|
|
71
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-TP4A72RF.js";
|
|
72
74
|
|
|
73
75
|
// src/main.ts
|
|
74
76
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
77
|
+
import * as crypto from "node:crypto";
|
|
75
78
|
import * as fs20 from "node:fs";
|
|
76
79
|
import { createRequire } from "node:module";
|
|
77
80
|
import * as net5 from "node:net";
|
|
@@ -853,6 +856,421 @@ var EDGE_CADDYFILE = [
|
|
|
853
856
|
"}"
|
|
854
857
|
].join("\n") + "\n";
|
|
855
858
|
|
|
859
|
+
// src/server-mirrors.ts
|
|
860
|
+
var NPM_MIRROR_IMAGE = "nginx:1.29-alpine";
|
|
861
|
+
var DOCKER_MIRROR_IMAGE = "registry:3.1";
|
|
862
|
+
var MIRRORS_COMPOSE_FILE = "compose.mirrors.yaml";
|
|
863
|
+
var NPM_MIRROR_CONF_NAME = "npm-mirror.conf";
|
|
864
|
+
var NPM_MIRROR_URL = "http://npm-mirror:4873/";
|
|
865
|
+
var DOCKER_MIRROR_URL = "http://docker-mirror:5000";
|
|
866
|
+
var DOCKER_MIRROR_CONTAINER = "mend-docker-mirror";
|
|
867
|
+
var DEFAULT_NPM_MIRROR_MAX_SIZE = "10g";
|
|
868
|
+
var DOCKER_MIRROR_TTL = "168h";
|
|
869
|
+
var DEFAULT_DOCKER_MIRROR_MAX_SIZE = "20g";
|
|
870
|
+
var MIRROR_MIN_FREE = "5g";
|
|
871
|
+
var DOCKER_MIRROR_GUARD_NAME = "docker-mirror-guard.sh";
|
|
872
|
+
var dockerMirrorLogin = (docker2) => docker2 !== null && "upstreamUser" in docker2 ? docker2.upstreamUser : void 0;
|
|
873
|
+
var DEFAULT_MIRRORS = {
|
|
874
|
+
npm: { maxSize: DEFAULT_NPM_MIRROR_MAX_SIZE },
|
|
875
|
+
docker: { maxSize: DEFAULT_DOCKER_MIRROR_MAX_SIZE }
|
|
876
|
+
};
|
|
877
|
+
var parseMirrorSize = (value) => {
|
|
878
|
+
const match = /^([1-9]\d{0,5})([mMgG])$/.exec(value.trim());
|
|
879
|
+
if (match === null) return null;
|
|
880
|
+
const [, amount = "", unit = ""] = match;
|
|
881
|
+
const size = `${amount}${unit.toLowerCase()}`;
|
|
882
|
+
return mirrorSizeBytes(size) >= 1024 ** 3 ? size : null;
|
|
883
|
+
};
|
|
884
|
+
var mirrorSizeBytes = (size) => {
|
|
885
|
+
const amount = Number(size.slice(0, -1));
|
|
886
|
+
return size.endsWith("g") ? amount * 1024 ** 3 : amount * 1024 ** 2;
|
|
887
|
+
};
|
|
888
|
+
var CREDENTIAL = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,254}$/;
|
|
889
|
+
var isDockerHubCredential = (value) => CREDENTIAL.test(value);
|
|
890
|
+
var runsMirrors = (mirrors) => mirrors !== void 0 && (mirrors.npm !== null || mirrors.docker !== null);
|
|
891
|
+
var mirrorsEnvLines = (mirrors, dockerHubToken) => {
|
|
892
|
+
const user = dockerMirrorLogin(mirrors?.docker ?? null);
|
|
893
|
+
return [
|
|
894
|
+
...mirrors === void 0 || mirrors.npm === null ? [] : [`MEND_NPM_MIRROR_MAX_SIZE=${mirrors.npm.maxSize}`],
|
|
895
|
+
...mirrors === void 0 || mirrors.docker === null ? [] : [`MEND_DOCKER_MIRROR_MAX_SIZE=${mirrors.docker.maxSize}`],
|
|
896
|
+
...user === void 0 || dockerHubToken === void 0 ? [] : [`MEND_DOCKER_HUB_USERNAME=${user}`, `MEND_DOCKER_HUB_TOKEN=${dockerHubToken}`]
|
|
897
|
+
];
|
|
898
|
+
};
|
|
899
|
+
var LOGGING = [
|
|
900
|
+
" logging:",
|
|
901
|
+
" driver: json-file",
|
|
902
|
+
" options:",
|
|
903
|
+
' max-size: "20m"',
|
|
904
|
+
' max-file: "5"'
|
|
905
|
+
];
|
|
906
|
+
var HEALTH_TIMING = [
|
|
907
|
+
" interval: 10s",
|
|
908
|
+
" timeout: 5s",
|
|
909
|
+
" retries: 3",
|
|
910
|
+
" start_period: 10s"
|
|
911
|
+
];
|
|
912
|
+
var npmMirrorService = () => [
|
|
913
|
+
" # Read-through cache of registry.npmjs.org (npm-mirror.conf). Size-capped, and it leaves",
|
|
914
|
+
` # ${MIRROR_MIN_FREE} free on its disk; the least recently used tarballs go first. No credential sent.`,
|
|
915
|
+
" npm-mirror:",
|
|
916
|
+
` image: ${NPM_MIRROR_IMAGE}`,
|
|
917
|
+
" hostname: npm-mirror",
|
|
918
|
+
" restart: unless-stopped",
|
|
919
|
+
" environment:",
|
|
920
|
+
' NGINX_ENTRYPOINT_LOCAL_RESOLVERS: "1"',
|
|
921
|
+
' NGINX_ENTRYPOINT_QUIET_LOGS: "1"',
|
|
922
|
+
" NPM_MIRROR_MAX_SIZE: ${MEND_NPM_MIRROR_MAX_SIZE:?set MEND_NPM_MIRROR_MAX_SIZE in server.env}",
|
|
923
|
+
` NPM_MIRROR_MIN_FREE: ${MIRROR_MIN_FREE}`,
|
|
924
|
+
" volumes:",
|
|
925
|
+
` - ./${NPM_MIRROR_CONF_NAME}:/etc/nginx/templates/default.conf.template:ro`,
|
|
926
|
+
" - mend-npm-mirror:/var/cache/npm-mirror",
|
|
927
|
+
" # Answered by nginx itself, so it says the mirror is up without asking the registry.",
|
|
928
|
+
" healthcheck:",
|
|
929
|
+
' test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:4873/-/ping"]',
|
|
930
|
+
...HEALTH_TIMING,
|
|
931
|
+
...LOGGING
|
|
932
|
+
];
|
|
933
|
+
var dockerMirrorService = (mirrors) => [
|
|
934
|
+
" # Pull-through cache of Docker Hub, run by its guard (docker-mirror-guard.sh): the cache stays",
|
|
935
|
+
` # under its cap and leaves ${MIRROR_MIN_FREE} free on its disk. Layer data is deleted seven days after a`,
|
|
936
|
+
" # fetch. The metrics listener stays on the container's loopback.",
|
|
937
|
+
" docker-mirror:",
|
|
938
|
+
` image: ${DOCKER_MIRROR_IMAGE}`,
|
|
939
|
+
` container_name: ${DOCKER_MIRROR_CONTAINER}`,
|
|
940
|
+
" hostname: docker-mirror",
|
|
941
|
+
" restart: unless-stopped",
|
|
942
|
+
" init: true",
|
|
943
|
+
` entrypoint: ["/bin/sh", "/mend/${DOCKER_MIRROR_GUARD_NAME}"]`,
|
|
944
|
+
" environment:",
|
|
945
|
+
" DOCKER_MIRROR_MAX_SIZE:",
|
|
946
|
+
" ${MEND_DOCKER_MIRROR_MAX_SIZE:?set MEND_DOCKER_MIRROR_MAX_SIZE in server.env}",
|
|
947
|
+
` DOCKER_MIRROR_MIN_FREE: ${MIRROR_MIN_FREE}`,
|
|
948
|
+
" REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io",
|
|
949
|
+
` REGISTRY_PROXY_TTL: ${DOCKER_MIRROR_TTL}`,
|
|
950
|
+
" REGISTRY_LOG_LEVEL: warn",
|
|
951
|
+
' REGISTRY_LOG_ACCESSLOG_DISABLED: "true"',
|
|
952
|
+
" REGISTRY_HTTP_DEBUG_ADDR: 127.0.0.1:5001",
|
|
953
|
+
' REGISTRY_HTTP_DEBUG_PROMETHEUS_ENABLED: "true"',
|
|
954
|
+
...dockerMirrorLogin(mirrors.docker) === void 0 ? [] : [
|
|
955
|
+
" # A token the operator declared Public Repo Read-only: every session can pull what it reads.",
|
|
956
|
+
" REGISTRY_PROXY_USERNAME: ${MEND_DOCKER_HUB_USERNAME:?set MEND_DOCKER_HUB_USERNAME in server.env}",
|
|
957
|
+
" REGISTRY_PROXY_PASSWORD: ${MEND_DOCKER_HUB_TOKEN:?set MEND_DOCKER_HUB_TOKEN in server.env}"
|
|
958
|
+
],
|
|
959
|
+
" volumes:",
|
|
960
|
+
` - ./${DOCKER_MIRROR_GUARD_NAME}:/mend/${DOCKER_MIRROR_GUARD_NAME}:ro`,
|
|
961
|
+
" - mend-docker-mirror:/var/lib/registry",
|
|
962
|
+
" # Healthy while its guard keeps working, which it records every pass. The registry inside may",
|
|
963
|
+
" # be paused for want of disk, or not started because Docker Hub did not answer (it asks Hub as",
|
|
964
|
+
" # it starts, and the guard tries again every pass): setup does not wait for it.",
|
|
965
|
+
" healthcheck:",
|
|
966
|
+
` test: ["CMD-SHELL", 'test -n "$$(find /tmp/mend-mirror-guard -mmin -2)"']`,
|
|
967
|
+
...HEALTH_TIMING,
|
|
968
|
+
...LOGGING
|
|
969
|
+
];
|
|
970
|
+
var renderMirrorsOverlay = (mirrors) => {
|
|
971
|
+
if (!runsMirrors(mirrors)) return void 0;
|
|
972
|
+
return [
|
|
973
|
+
"# Written by mend server setup: the package and image mirrors this install runs, on the Compose",
|
|
974
|
+
"# network only (no host port). Values come from server.env. Sessions install npm packages and",
|
|
975
|
+
"# pull Docker Hub images through them; a mirror that is down sends them to the registry itself.",
|
|
976
|
+
"services:",
|
|
977
|
+
" mend:",
|
|
978
|
+
" environment:",
|
|
979
|
+
...mirrors.npm === null ? [] : [` MEND_NPM_MIRROR_URL: ${NPM_MIRROR_URL}`],
|
|
980
|
+
...mirrors.docker === null ? [] : [
|
|
981
|
+
` SEALANT_DOCKER_REGISTRY_MIRRORS: ${DOCKER_MIRROR_URL}`,
|
|
982
|
+
` SEALANT_DOCKER_REGISTRY_MIRROR_CONTAINER: ${DOCKER_MIRROR_CONTAINER}`
|
|
983
|
+
],
|
|
984
|
+
"",
|
|
985
|
+
...mirrors.npm === null ? [] : [...npmMirrorService(), ""],
|
|
986
|
+
...mirrors.docker === null ? [] : [...dockerMirrorService(mirrors), ""],
|
|
987
|
+
"volumes:",
|
|
988
|
+
...mirrors.npm === null ? [] : [" mend-npm-mirror:"],
|
|
989
|
+
...mirrors.docker === null ? [] : [" mend-docker-mirror:"],
|
|
990
|
+
""
|
|
991
|
+
].join("\n");
|
|
992
|
+
};
|
|
993
|
+
var mirrorServices = (mirrors) => [
|
|
994
|
+
...mirrors === void 0 || mirrors.npm === null ? [] : ["npm-mirror"],
|
|
995
|
+
...mirrors === void 0 || mirrors.docker === null ? [] : ["docker-mirror"]
|
|
996
|
+
];
|
|
997
|
+
var mirrorImagesOf = (overlay) => [...overlay.matchAll(/^ {4}image:[ \t]*((?:nginx|registry):[^\s#]+)[ \t]*$/gm)].map((match) => match[1]).filter((image) => image !== void 0);
|
|
998
|
+
var NPM_MIRROR_CONF = `# Mend's npm mirror: a read-through cache of the public npm registry (registry.npmjs.org) for the
|
|
999
|
+
# workspaces on this host. nginx keeps what it fetched and serves it again. The package manager
|
|
1000
|
+
# checks every tarball against its lockfile's integrity hash, so the cache never decides what is
|
|
1001
|
+
# installed. Nothing is published through it, and no credential is sent upstream. When the
|
|
1002
|
+
# registry fails, a copy already held is served instead.
|
|
1003
|
+
proxy_cache_path /var/cache/npm-mirror levels=2:2 keys_zone=npm:64m
|
|
1004
|
+
max_size=\${NPM_MIRROR_MAX_SIZE} min_free=\${NPM_MIRROR_MIN_FREE}
|
|
1005
|
+
inactive=90d use_temp_path=off;
|
|
1006
|
+
|
|
1007
|
+
# Abbreviated metadata (what installs ask for) and full metadata are different documents.
|
|
1008
|
+
map $http_accept $npm_document {
|
|
1009
|
+
"~application/vnd\\.npm\\.install-v1\\+json" abbreviated;
|
|
1010
|
+
default full;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
# One JSON line per request: \`mend server status\` reads the cache field of the tarball lines.
|
|
1014
|
+
log_format npm_mirror escape=json
|
|
1015
|
+
'{"time":"$time_iso8601","method":"$request_method","uri":"$uri","status":$status,'
|
|
1016
|
+
'"cache":"$upstream_cache_status","bytes":$body_bytes_sent}';
|
|
1017
|
+
|
|
1018
|
+
server {
|
|
1019
|
+
listen 4873;
|
|
1020
|
+
access_log /dev/stdout npm_mirror;
|
|
1021
|
+
error_log /dev/stderr warn;
|
|
1022
|
+
client_max_body_size 8m;
|
|
1023
|
+
|
|
1024
|
+
resolver \${NGINX_LOCAL_RESOLVERS} valid=60s ipv6=off;
|
|
1025
|
+
resolver_timeout 5s;
|
|
1026
|
+
set $npm_registry registry.npmjs.org;
|
|
1027
|
+
|
|
1028
|
+
proxy_http_version 1.1;
|
|
1029
|
+
proxy_set_header Host registry.npmjs.org;
|
|
1030
|
+
proxy_set_header Connection "";
|
|
1031
|
+
# Never forward a credential: the mirror serves the public registry, the same to everyone.
|
|
1032
|
+
proxy_set_header Authorization "";
|
|
1033
|
+
proxy_set_header Cookie "";
|
|
1034
|
+
# Uncompressed from upstream, so one cached copy serves every client; gzip below compresses it.
|
|
1035
|
+
proxy_set_header Accept-Encoding "";
|
|
1036
|
+
proxy_ssl_server_name on;
|
|
1037
|
+
proxy_ssl_name registry.npmjs.org;
|
|
1038
|
+
proxy_ssl_verify on;
|
|
1039
|
+
proxy_ssl_verify_depth 4;
|
|
1040
|
+
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
|
1041
|
+
proxy_connect_timeout 10s;
|
|
1042
|
+
proxy_read_timeout 60s;
|
|
1043
|
+
proxy_ignore_headers Cache-Control Expires Set-Cookie Vary;
|
|
1044
|
+
proxy_hide_header Set-Cookie;
|
|
1045
|
+
proxy_cache_lock on;
|
|
1046
|
+
proxy_cache_lock_timeout 120s;
|
|
1047
|
+
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_429;
|
|
1048
|
+
proxy_cache_background_update on;
|
|
1049
|
+
add_header X-Mend-Mirror $upstream_cache_status always;
|
|
1050
|
+
|
|
1051
|
+
gzip on;
|
|
1052
|
+
gzip_proxied any;
|
|
1053
|
+
gzip_types application/json application/vnd.npm.install-v1+json;
|
|
1054
|
+
|
|
1055
|
+
# Answered here, so a client can tell the mirror is up without asking the registry.
|
|
1056
|
+
location = /-/ping {
|
|
1057
|
+
default_type application/json;
|
|
1058
|
+
return 200 '{}';
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
# Tarballs never change once published: kept until the cache needs the room.
|
|
1062
|
+
location ~ ^/(?:@[^/]+/)?[^/]+/-/[^/]+\\.tgz$ {
|
|
1063
|
+
limit_except GET HEAD { deny all; }
|
|
1064
|
+
proxy_cache npm;
|
|
1065
|
+
proxy_cache_key $uri;
|
|
1066
|
+
proxy_cache_valid 200 365d;
|
|
1067
|
+
proxy_pass https://$npm_registry;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
# Audit reports go to the registry as they are, never cached.
|
|
1071
|
+
location /-/npm/v1/security/ {
|
|
1072
|
+
limit_except POST { deny all; }
|
|
1073
|
+
proxy_pass https://$npm_registry;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
# Metadata: fresh for five minutes, then revalidated; served stale while the registry fails.
|
|
1077
|
+
# The key is the whole request, query included: a search for one package never answers another.
|
|
1078
|
+
location / {
|
|
1079
|
+
limit_except GET HEAD { deny all; }
|
|
1080
|
+
proxy_cache npm;
|
|
1081
|
+
proxy_cache_key $request_uri|$npm_document;
|
|
1082
|
+
proxy_cache_valid 200 5m;
|
|
1083
|
+
proxy_cache_valid 404 1m;
|
|
1084
|
+
proxy_cache_revalidate on;
|
|
1085
|
+
proxy_pass https://$npm_registry;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
`;
|
|
1089
|
+
var DOCKER_MIRROR_GUARD = `#!/bin/sh
|
|
1090
|
+
# Mend's Docker mirror guard (written by mend server setup): runs the registry, and keeps its cache
|
|
1091
|
+
# under a byte cap and the disk it lives on above a floor. Over the cap, the cache is cleared and
|
|
1092
|
+
# fills again from Docker Hub. Below the floor, the cache is cleared and the registry stays stopped
|
|
1093
|
+
# until there is room again; meanwhile session Docker daemons pull from Docker Hub directly. The
|
|
1094
|
+
# registry has no size cap of its own, and its seven-day expiry deletes layer data only.
|
|
1095
|
+
set -u
|
|
1096
|
+
root=/var/lib/registry
|
|
1097
|
+
state=/tmp/mend-mirror-guard
|
|
1098
|
+
size_mb() {
|
|
1099
|
+
case "$1" in
|
|
1100
|
+
*g) echo $((\${1%g} * 1024)) ;;
|
|
1101
|
+
*m) echo "\${1%m}" ;;
|
|
1102
|
+
*) echo "mend docker mirror guard: cannot read size $1" >&2 && exit 64 ;;
|
|
1103
|
+
esac
|
|
1104
|
+
}
|
|
1105
|
+
cap=$(size_mb "\${DOCKER_MIRROR_MAX_SIZE:?}") || exit 64
|
|
1106
|
+
floor=$(size_mb "\${DOCKER_MIRROR_MIN_FREE:?}") || exit 64
|
|
1107
|
+
interval=\${DOCKER_MIRROR_GUARD_INTERVAL:-30}
|
|
1108
|
+
pid=
|
|
1109
|
+
stop_registry() {
|
|
1110
|
+
if [ -n "$pid" ]; then
|
|
1111
|
+
kill "$pid" 2>/dev/null
|
|
1112
|
+
wait "$pid" 2>/dev/null
|
|
1113
|
+
fi
|
|
1114
|
+
pid=
|
|
1115
|
+
}
|
|
1116
|
+
clear_cache() { rm -rf "$root/docker" "$root/scheduler-state.json"; }
|
|
1117
|
+
cache_held() { [ -e "$root/docker" ] || [ -e "$root/scheduler-state.json" ]; }
|
|
1118
|
+
trap 'stop_registry; exit 0' TERM INT
|
|
1119
|
+
while :; do
|
|
1120
|
+
free=$(df -Pm "$root" | awk 'NR == 2 { print $4 }')
|
|
1121
|
+
if [ "$free" -lt "$floor" ]; then
|
|
1122
|
+
# Below the floor: no registry, and no cache, whether the registry was running or this is a
|
|
1123
|
+
# start with a cache left from before. Once the space is back, the next pass starts it again.
|
|
1124
|
+
stop_registry
|
|
1125
|
+
# What is left, looked at again after clearing: the log and the status say what this finds,
|
|
1126
|
+
# not what was attempted.
|
|
1127
|
+
if cache_held; then
|
|
1128
|
+
clear_cache
|
|
1129
|
+
if cache_held; then outcome="cache could not be cleared"; else outcome="cache cleared"; fi
|
|
1130
|
+
echo "mend docker mirror guard: \${free} MiB free on its disk, below \${floor} MiB: \${outcome}, registry paused" >&2
|
|
1131
|
+
fi
|
|
1132
|
+
if cache_held; then held=kept; else held=none; fi
|
|
1133
|
+
echo "paused \${free} \${floor} \${held}" >"$state"
|
|
1134
|
+
else
|
|
1135
|
+
used=$(du -sm "$root" | cut -f1)
|
|
1136
|
+
if [ "$used" -gt "$cap" ]; then
|
|
1137
|
+
stop_registry
|
|
1138
|
+
clear_cache
|
|
1139
|
+
if cache_held; then outcome="could not be cleared"; else outcome="cleared"; fi
|
|
1140
|
+
echo "mend docker mirror guard: cache \${used} MiB, over its \${cap} MiB cap: \${outcome}" >&2
|
|
1141
|
+
fi
|
|
1142
|
+
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
|
|
1143
|
+
registry serve /etc/distribution/config.yml &
|
|
1144
|
+
pid=$!
|
|
1145
|
+
fi
|
|
1146
|
+
echo "running" >"$state"
|
|
1147
|
+
fi
|
|
1148
|
+
sleep "$interval" &
|
|
1149
|
+
wait $!
|
|
1150
|
+
done
|
|
1151
|
+
`;
|
|
1152
|
+
var formatBytes = (bytes) => {
|
|
1153
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1154
|
+
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
1155
|
+
let value = bytes / 1024;
|
|
1156
|
+
let unit = 0;
|
|
1157
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1158
|
+
value /= 1024;
|
|
1159
|
+
unit += 1;
|
|
1160
|
+
}
|
|
1161
|
+
return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
|
|
1162
|
+
};
|
|
1163
|
+
var percent = (part, whole) => whole === 0 ? "" : ` (${Math.round(part / whole * 100)}%)`;
|
|
1164
|
+
var mirrorDiskProbe = (directory) => `du -sk ${directory} | cut -f1; df -Pk ${directory} | awk 'NR == 2 { print $4 }'; cat /tmp/mend-mirror-guard 2>/dev/null || true`;
|
|
1165
|
+
var mirrorDiskOf = (stdout) => {
|
|
1166
|
+
const [used, free, guard = ""] = stdout.trim().split("\n").map((line) => line.trim());
|
|
1167
|
+
const usedKiB = Number(used);
|
|
1168
|
+
const freeKiB = Number(free);
|
|
1169
|
+
if (used === void 0 || free === void 0 || used === "" || free === "") return null;
|
|
1170
|
+
if (!Number.isFinite(usedKiB) || !Number.isFinite(freeKiB)) return null;
|
|
1171
|
+
const paused = /^paused (\d+) (\d+)(?: (none|kept))?$/.exec(guard);
|
|
1172
|
+
return {
|
|
1173
|
+
used: usedKiB * 1024,
|
|
1174
|
+
free: freeKiB * 1024,
|
|
1175
|
+
guard: paused?.[1] !== void 0 && paused[2] !== void 0 ? {
|
|
1176
|
+
state: "paused",
|
|
1177
|
+
freeMiB: Number(paused[1]),
|
|
1178
|
+
floorMiB: Number(paused[2]),
|
|
1179
|
+
cache: paused[3] === "none" || paused[3] === "kept" ? paused[3] : null
|
|
1180
|
+
} : guard === "running" ? { state: "running" } : null
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
var FROM_CACHE = /* @__PURE__ */ new Set(["HIT", "STALE", "UPDATING", "REVALIDATED"]);
|
|
1184
|
+
var FROM_UPSTREAM = /* @__PURE__ */ new Set(["MISS", "EXPIRED", "BYPASS"]);
|
|
1185
|
+
var npmMirrorTraffic = (log) => {
|
|
1186
|
+
let tarballs = 0;
|
|
1187
|
+
let fromCache = 0;
|
|
1188
|
+
let fromRegistry = 0;
|
|
1189
|
+
for (const line of log.split("\n")) {
|
|
1190
|
+
const start = line.indexOf("{");
|
|
1191
|
+
if (start < 0) continue;
|
|
1192
|
+
let fields;
|
|
1193
|
+
try {
|
|
1194
|
+
fields = JSON.parse(line.slice(start));
|
|
1195
|
+
} catch {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (typeof fields !== "object" || fields === null) continue;
|
|
1199
|
+
const entry = new Map(Object.entries(fields));
|
|
1200
|
+
const uri = entry.get("uri");
|
|
1201
|
+
const cache = entry.get("cache");
|
|
1202
|
+
if (typeof uri !== "string" || !uri.endsWith(".tgz") || typeof cache !== "string") continue;
|
|
1203
|
+
tarballs += 1;
|
|
1204
|
+
if (FROM_CACHE.has(cache)) fromCache += 1;
|
|
1205
|
+
else if (FROM_UPSTREAM.has(cache)) fromRegistry += 1;
|
|
1206
|
+
}
|
|
1207
|
+
return { tarballs, fromCache, fromRegistry };
|
|
1208
|
+
};
|
|
1209
|
+
var dockerMirrorTraffic = (metrics) => {
|
|
1210
|
+
const counter = (name, type2) => {
|
|
1211
|
+
const match = new RegExp(`^registry_proxy_${name}_total\\{type="${type2}"\\} (\\d+)`, "m").exec(
|
|
1212
|
+
metrics
|
|
1213
|
+
);
|
|
1214
|
+
return match?.[1] === void 0 ? null : Number(match[1]);
|
|
1215
|
+
};
|
|
1216
|
+
const values = [
|
|
1217
|
+
counter("hits", "blob"),
|
|
1218
|
+
counter("misses", "blob"),
|
|
1219
|
+
counter("hits", "manifest"),
|
|
1220
|
+
counter("misses", "manifest")
|
|
1221
|
+
];
|
|
1222
|
+
const [blobHits, blobMisses, manifestHits, manifestMisses] = values;
|
|
1223
|
+
if (blobHits === void 0 || blobHits === null || blobMisses === void 0 || blobMisses === null || manifestHits === void 0 || manifestHits === null || manifestMisses === void 0 || manifestMisses === null) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
blobs: { hits: blobHits, misses: blobMisses },
|
|
1228
|
+
manifests: { hits: manifestHits, misses: manifestMisses }
|
|
1229
|
+
};
|
|
1230
|
+
};
|
|
1231
|
+
var diskWords = (disk, cap) => typeof disk === "string" ? `size not read: ${disk} \xB7 cap ${formatBytes(mirrorSizeBytes(cap))}` : `${formatBytes(disk.used)} cached of ${formatBytes(mirrorSizeBytes(cap))} \xB7 ${formatBytes(disk.free)} free on its disk`;
|
|
1232
|
+
var observedNpmMirrorLine = (mirrors, observed) => {
|
|
1233
|
+
if (mirrors.npm === null)
|
|
1234
|
+
return "npm mirror \xB7 off on this install \xB7 mend server setup --npm-mirror turns it on";
|
|
1235
|
+
if (!observed.running)
|
|
1236
|
+
return "npm mirror \xB7 container not running \xB7 sessions install from registry.npmjs.org directly";
|
|
1237
|
+
const { traffic } = observed;
|
|
1238
|
+
const seen = typeof traffic === "string" ? `traffic not read: ${traffic}` : traffic.tarballs === 0 ? "no tarball requests in its log for the last 24 h" : `last 24 h: ${traffic.tarballs} tarball requests \xB7 ${traffic.fromCache} served from the cache${percent(traffic.fromCache, traffic.tarballs)} \xB7 ${traffic.fromRegistry} fetched from registry.npmjs.org`;
|
|
1239
|
+
return [
|
|
1240
|
+
"npm mirror \xB7 running",
|
|
1241
|
+
diskWords(observed.disk, mirrors.npm.maxSize),
|
|
1242
|
+
seen,
|
|
1243
|
+
"observed"
|
|
1244
|
+
].join(" \xB7 ");
|
|
1245
|
+
};
|
|
1246
|
+
var observedDockerMirrorLine = (mirrors, observed) => {
|
|
1247
|
+
if (mirrors.docker === null)
|
|
1248
|
+
return "docker mirror \xB7 off on this install \xB7 mend server setup --docker-mirror turns it on";
|
|
1249
|
+
if (!observed.running)
|
|
1250
|
+
return "docker mirror \xB7 container not running \xB7 session Docker daemons pull from Docker Hub directly";
|
|
1251
|
+
const { disk, traffic } = observed;
|
|
1252
|
+
if (typeof disk !== "string" && disk.guard?.state === "paused")
|
|
1253
|
+
return [
|
|
1254
|
+
"docker mirror \xB7 paused by its disk guard",
|
|
1255
|
+
`${formatBytes(disk.guard.freeMiB * 1024 ** 2)} free on its disk, below ${formatBytes(disk.guard.floorMiB * 1024 ** 2)}`,
|
|
1256
|
+
disk.guard.cache === "none" ? "no cache held" : disk.guard.cache === "kept" ? "its cache could not be cleared" : "the guard did not say whether its cache was cleared",
|
|
1257
|
+
"session Docker daemons pull from Docker Hub directly until there is room",
|
|
1258
|
+
"observed"
|
|
1259
|
+
].join(" \xB7 ");
|
|
1260
|
+
const since = observed.startedAt === null ? "since it started" : `since ${observed.startedAt}`;
|
|
1261
|
+
const seen = typeof traffic === "string" ? `traffic not read: ${traffic}` : `${since}: layers ${traffic.blobs.hits + traffic.blobs.misses} requested \xB7 ${traffic.blobs.hits} from the cache${percent(traffic.blobs.hits, traffic.blobs.hits + traffic.blobs.misses)} \xB7 manifests ${traffic.manifests.hits + traffic.manifests.misses} \xB7 ${traffic.manifests.hits} from the cache`;
|
|
1262
|
+
const user = dockerMirrorLogin(mirrors.docker);
|
|
1263
|
+
const login2 = user === void 0 ? "pulls from Docker Hub anonymously" : `pulls from Docker Hub as ${user}, with a token the operator declared Public Repo Read-only \xB7 every session can pull whatever that token can read`;
|
|
1264
|
+
return [
|
|
1265
|
+
"docker mirror \xB7 running",
|
|
1266
|
+
diskWords(disk, mirrors.docker.maxSize),
|
|
1267
|
+
"layers evicted 7 days after each fetch",
|
|
1268
|
+
seen,
|
|
1269
|
+
login2,
|
|
1270
|
+
"observed"
|
|
1271
|
+
].join(" \xB7 ");
|
|
1272
|
+
};
|
|
1273
|
+
|
|
856
1274
|
// src/server-edge.ts
|
|
857
1275
|
var EXPOSURES = ["loopback", "private", "public"];
|
|
858
1276
|
var TENANCIES = ["single", "multi"];
|
|
@@ -861,6 +1279,9 @@ var isTenancy = (value) => TENANCIES.some((tenancy) => tenancy === value);
|
|
|
861
1279
|
var EDGE_IMAGE = "caddy:2.10-alpine";
|
|
862
1280
|
var EDGE_COMPOSE_FILE = "compose.edge.yaml";
|
|
863
1281
|
var POSTURE_COMPOSE_FILE = "compose.posture.yaml";
|
|
1282
|
+
var DECLARABLE_ITEMS = ["core-private", "edge-tls", "workspace-ssh"];
|
|
1283
|
+
var isDeclarableItem = (value) => DECLARABLE_ITEMS.some((item) => item === value);
|
|
1284
|
+
var publishedAddress = (address, port) => `${net2.isIP(address) === 6 ? `[${address}]` : address}:${port}`;
|
|
864
1285
|
var parseEdgeHost = (value) => {
|
|
865
1286
|
const host = value.trim().toLowerCase().replace(/\.$/, "");
|
|
866
1287
|
const labels = host.split(".");
|
|
@@ -876,7 +1297,11 @@ var postureEnvironment = (posture) => {
|
|
|
876
1297
|
...posture.exposure === void 0 ? [] : [["MEND_EXPOSURE", posture.exposure]],
|
|
877
1298
|
...posture.tenancy === void 0 ? [] : [["MEND_TENANCY", posture.tenancy]],
|
|
878
1299
|
...gate ? [["MEND_SOURCE_POLICY", "tenant"], ["MEND_CAPTURE_REQUIRE_SIZES", "true"]] : [],
|
|
879
|
-
...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : []
|
|
1300
|
+
...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : [],
|
|
1301
|
+
// The gate's workspace-ssh item reads where SSH is published apart from the web port; the
|
|
1302
|
+
// container cannot see what its host publishes.
|
|
1303
|
+
...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [["MEND_SSH_PUBLISHED", publishedAddress(posture.sshBind, posture.sshPort)]],
|
|
1304
|
+
...posture.declared === void 0 || posture.declared.length === 0 ? [] : [["MEND_EXPOSURE_DECLARED", posture.declared.join(",")]]
|
|
880
1305
|
];
|
|
881
1306
|
};
|
|
882
1307
|
var postureEnvLines = (posture) => [
|
|
@@ -902,7 +1327,8 @@ var renderPostureOverlay = (posture) => {
|
|
|
902
1327
|
};
|
|
903
1328
|
var composeOverlays = (posture) => [
|
|
904
1329
|
...posture.edgeHost === void 0 ? [] : [EDGE_COMPOSE_FILE],
|
|
905
|
-
...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE]
|
|
1330
|
+
...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE],
|
|
1331
|
+
...runsMirrors(posture.mirrors) ? [MIRRORS_COMPOSE_FILE] : []
|
|
906
1332
|
];
|
|
907
1333
|
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? new Map(Object.entries(value)) : null;
|
|
908
1334
|
var healthPosture = (body) => {
|
|
@@ -927,7 +1353,11 @@ var healthPosture = (body) => {
|
|
|
927
1353
|
var declaredPostureLines = (posture) => [
|
|
928
1354
|
...posture.edgeHost === void 0 ? [] : [`edge \xB7 ${posture.edgeHost} \xB7 ${EDGE_IMAGE} on 80 and 443 \xB7 Mend's own port on loopback`],
|
|
929
1355
|
`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" : ""}
|
|
1356
|
+
`tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`,
|
|
1357
|
+
...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [
|
|
1358
|
+
`workspace ssh \xB7 published on ${publishedAddress(posture.sshBind, posture.sshPort)} apart from the web port`
|
|
1359
|
+
],
|
|
1360
|
+
...posture.declared === void 0 || posture.declared.length === 0 ? [] : [`stated verified from outside \xB7 ${posture.declared.join(", ")}`]
|
|
931
1361
|
];
|
|
932
1362
|
var observedEdgeLine = (host, observed) => [
|
|
933
1363
|
`edge \xB7 ${host}`,
|
|
@@ -1969,7 +2399,7 @@ var COMMANDS = [
|
|
|
1969
2399
|
"The same worktree, record, and review as mend codex, with a command of your own in place of a harness. Everything after -- is the command.",
|
|
1970
2400
|
"The command's output is printed as the record has it, and mend run exits with the command's exit code. It runs in a terminal, so stdout and stderr arrive together, on stdout. What Mend says itself goes to stderr, so out=$(mend run -- git log -1) holds the command's output and nothing else. Ctrl+C stops watching and puts the terminal back; the command keeps running, and mend logs and mend wait pick it up again. A signal exits 128 + its number: 130 for SIGINT (Ctrl+C), 129 for SIGHUP, 143 for SIGTERM. A second signal exits at once, and an exit waits at most 5 seconds for a reader that takes nothing, then says the output may be incomplete.",
|
|
1971
2401
|
"Output this terminal could not be given in full (a read the server refused, a reader that went away) fails mend run with exit 1 even when the command succeeded, and the command's own code is said on stderr: a script never takes cut output for the whole of it.",
|
|
1972
|
-
"The platform takes at most 64 words,
|
|
2402
|
+
"The platform takes at most 64 words and 1 MiB, at most 131,071 bytes a word, a program with no leading or trailing whitespace, and no NUL byte. An argument may be empty, start with a newline or span lines. A command it would refuse is refused before anything is created.",
|
|
1973
2403
|
"With --detach, mend run returns once the command runs. With --json, stdout carries one JSON object in place of the output: the session id, the process id, the worktree, the branch, and the process's status and exit code as last observed, which is how it ended without --detach (and with it, when the command ended first).",
|
|
1974
2404
|
"Workspaces set PAGER=cat, so git log and friends print instead of waiting for a pager the image does not have. A project variable of the same name, or the pager in your own git config, wins."
|
|
1975
2405
|
],
|
|
@@ -1994,31 +2424,39 @@ var COMMANDS = [
|
|
|
1994
2424
|
name: "logs",
|
|
1995
2425
|
section: "sessions",
|
|
1996
2426
|
summary: "print a session's recorded terminal output",
|
|
1997
|
-
synopsis: [
|
|
2427
|
+
synopsis: [
|
|
2428
|
+
"[session] [--follow] [--from <sequence>] [--process <id>]",
|
|
2429
|
+
"[session] --service <name-or-id> [--follow] [--from <sequence>]"
|
|
2430
|
+
],
|
|
1998
2431
|
description: [
|
|
1999
2432
|
"Prints what the session's command (or agent) wrote to its terminal, as the record holds it, on stdout. Settled sessions count: the record outlives the process and the workspace. --follow keeps printing until the process ends. The record is printed as it is, nothing taken out: anyone who can read the project can read it, so what a command printed, a password included, reaches them too. A signal stops it and puts the terminal back, exiting 128 + its number: 130 for SIGINT (Ctrl+C), 129 for SIGHUP, 143 for SIGTERM.",
|
|
2000
|
-
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken. --process reads another process of the session, a shell or a Service attempt, by a prefix of its id."
|
|
2433
|
+
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken. --process reads another process of the session, a shell or a Service attempt, by a prefix of its id; a Service's id or name there reads the Service's current attempt.",
|
|
2434
|
+
"--service reads a Service's current attempt, by the Service's id, its name or a prefix of its id, the ids mend service list prints. A full id names its Service before any name does. A name two Services carry is refused, with both ids listed; name one by its id, or name the session. A Service with no attempt (an adopted port, which Mend runs no process for) has nothing recorded, and mend logs says so and exits 1."
|
|
2001
2435
|
],
|
|
2002
2436
|
options: [
|
|
2003
2437
|
{ flag: "--follow, -f", text: "keep printing until the process ends" },
|
|
2004
2438
|
{ flag: "--from <sequence>", text: "start at a record sequence. Default: 0" },
|
|
2005
|
-
{ flag: "--process <id>", text: "another process of the session, by a prefix of its id" }
|
|
2439
|
+
{ flag: "--process <id>", text: "another process of the session, by a prefix of its id" },
|
|
2440
|
+
{ flag: "--service <name-or-id>", text: "a Service's current attempt" }
|
|
2441
|
+
],
|
|
2442
|
+
examples: [
|
|
2443
|
+
{ command: "mend logs 3f2a --follow", text: "" },
|
|
2444
|
+
{ command: "mend logs --service web --follow", text: "the Service web's output, live" }
|
|
2006
2445
|
],
|
|
2007
|
-
examples: [{ command: "mend logs 3f2a --follow", text: "" }],
|
|
2008
2446
|
see: ["run", "wait", "service logs"]
|
|
2009
2447
|
},
|
|
2010
2448
|
{
|
|
2011
2449
|
name: "wait",
|
|
2012
2450
|
section: "sessions",
|
|
2013
2451
|
summary: "wait for a session's command to end",
|
|
2014
|
-
synopsis: ["[session] [--timeout <
|
|
2452
|
+
synopsis: ["[session] [--timeout <duration>] [--process <id>] [--json]"],
|
|
2015
2453
|
description: [
|
|
2016
2454
|
"Returns once the session's command (or agent) has ended, with its exit code: the code the platform reported, or 1 when it reported none. A command that already ended answers at once. While a launch or a resume is starting, the previous process's end does not count. --process waits for one process by its id, the processId mend run --json prints.",
|
|
2017
|
-
"When --timeout passes first, mend wait exits 124, as timeout(1) does, and the command keeps running. Stopped by a signal, it exits 128 + its number (130 for Ctrl+C), and the command keeps running. The timeout covers everything: finding the session, every read and every retry. --json then prints the last state read, and nothing more is asked of the server.",
|
|
2455
|
+
"A session that is still starting (its workspace building, its image pulling) is waited through: only its end counts. When --timeout passes first, mend wait exits 124, as timeout(1) does, and the command keeps running. A duration is seconds (90, 90s or .5), minutes (5m) or hours (1h); with none, mend wait waits as long as the command runs. Stopped by a signal, it exits 128 + its number (130 for Ctrl+C), and the command keeps running. The timeout covers everything: finding the session, every read and every retry. --json then prints the last state read, and nothing more is asked of the server.",
|
|
2018
2456
|
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken."
|
|
2019
2457
|
],
|
|
2020
2458
|
options: [
|
|
2021
|
-
{ flag: "--timeout <
|
|
2459
|
+
{ flag: "--timeout <duration>", text: "give up after this long (90, 90s, 5m, 1h); exit 124" },
|
|
2022
2460
|
{
|
|
2023
2461
|
flag: "--process <id>",
|
|
2024
2462
|
text: "wait for this process of the session, by a prefix of its id"
|
|
@@ -2219,18 +2657,22 @@ var COMMANDS = [
|
|
|
2219
2657
|
section: "services",
|
|
2220
2658
|
summary: "start and supervise a server in the session's workspace",
|
|
2221
2659
|
synopsis: [
|
|
2222
|
-
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait] [--no-connect] -- <command...>",
|
|
2223
|
-
"[session] <name> [--wait] [--no-connect]"
|
|
2660
|
+
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait [--timeout <duration>]] [--no-connect] -- <command...>",
|
|
2661
|
+
"[session] <name> [--wait [--timeout <duration>]] [--no-connect]"
|
|
2224
2662
|
],
|
|
2225
2663
|
description: [
|
|
2226
2664
|
"With --, the command after it is started in the workspace and supervised: its output is recorded, and mend service restart re-runs it. Without --, the name is a Service declared in the worktree's mend.toml. mend service <name> is the shorthand for that.",
|
|
2227
|
-
"Mend waits up to a minute for the port to answer before it returns. --wait
|
|
2665
|
+
"Mend waits up to a minute for the port to answer before it returns. --wait keeps waiting while the Service is still starting (its process runs and its port has not answered yet: building, installing, booting), however long that takes, up to --timeout (default 10 minutes). Mend probes a started Service's port every 20 seconds, so the wait ends at the first probe that answers. The wait judges the one process this start began, by an id the start sends and the server stamps on it, whatever another client starts, restarts or stops meanwhile. The exit status says how it ended: 0 the port answered for that process; 1 Mend refused the start or could not be asked; 2 the process ended before its port answered (the line says its status and exit code; a process its workspace took reads exited, no exit code reported); 3 the server no longer has the session, its workspace gone with it; 124 the Service was still starting when the timeout passed, and it keeps starting. A server older than this CLI stamps no id: a start whose answer an edge cut is then not followed (1), and a command that exits inside the minute reads as the refusal it answers with (1). A waited start returns and opens no tunnel; mend service connect reaches the port. UDP has no probe, and a recipe that declares only a port is adopted with one probe, so --wait takes neither.",
|
|
2228
2666
|
"The port is tunnelled to this machine's loopback as soon as it listens, unless --no-connect."
|
|
2229
2667
|
],
|
|
2230
2668
|
options: [
|
|
2231
2669
|
{
|
|
2232
2670
|
flag: "--wait",
|
|
2233
|
-
text: "
|
|
2671
|
+
text: "return once the port answers: 0; process ended 2, session gone 3; no tunnel"
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
flag: "--timeout <duration>",
|
|
2675
|
+
text: "bound --wait (90, 90s, 5m, 1h); still starting then exits 124. Default: 10m"
|
|
2234
2676
|
},
|
|
2235
2677
|
{ flag: "--port <port>", text: "the port the command listens on inside the workspace" },
|
|
2236
2678
|
{ flag: "--name <n>", text: "the service's name. Default: the command" },
|
|
@@ -2240,9 +2682,13 @@ var COMMANDS = [
|
|
|
2240
2682
|
],
|
|
2241
2683
|
examples: [
|
|
2242
2684
|
{ command: "mend service run --port 3000 -- pnpm dev", text: "" },
|
|
2243
|
-
{ command: "mend service web", text: "the Service named web in mend.toml" }
|
|
2685
|
+
{ command: "mend service web", text: "the Service named web in mend.toml" },
|
|
2686
|
+
{
|
|
2687
|
+
command: "mend service run web --wait --timeout 15m",
|
|
2688
|
+
text: "returns once web answers; 124 if it is still starting after 15 minutes"
|
|
2689
|
+
}
|
|
2244
2690
|
],
|
|
2245
|
-
see: ["service init", "service connect", "service logs"]
|
|
2691
|
+
see: ["service init", "service connect", "service logs", "logs"]
|
|
2246
2692
|
},
|
|
2247
2693
|
{
|
|
2248
2694
|
name: "service add",
|
|
@@ -2275,9 +2721,13 @@ var COMMANDS = [
|
|
|
2275
2721
|
name: "service list",
|
|
2276
2722
|
section: "services",
|
|
2277
2723
|
summary: "every live service and its observed state",
|
|
2278
|
-
synopsis: [],
|
|
2279
|
-
description: [
|
|
2280
|
-
|
|
2724
|
+
synopsis: ["[--json]"],
|
|
2725
|
+
description: [
|
|
2726
|
+
"Name, observed state, port, the Service's id and its current attempt's process id, and where it is reachable from here. The process id is what mend logs --process and mend wait --process take; mend logs --service takes the Service's name or id. An adopted port has no process.",
|
|
2727
|
+
"With --json, stdout carries one JSON object: version 1, and services, each with id, name, sessionId, processId (null for an adopted port), status as last observed, workspacePort, protocol (tcp or udp), and hostPort, authority and browserUrl (null when not bound)."
|
|
2728
|
+
],
|
|
2729
|
+
options: [{ flag: "--json", text: "print the Services as JSON" }],
|
|
2730
|
+
see: ["service run", "service connect", "logs"]
|
|
2281
2731
|
},
|
|
2282
2732
|
{
|
|
2283
2733
|
name: "service logs",
|
|
@@ -2710,7 +3160,7 @@ var COMMANDS = [
|
|
|
2710
3160
|
summary: "how this instance is exposed, as declared and as observed",
|
|
2711
3161
|
synopsis: [],
|
|
2712
3162
|
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
|
|
3163
|
+
"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
3164
|
],
|
|
2715
3165
|
see: ["operator gate", "doctor"]
|
|
2716
3166
|
},
|
|
@@ -2796,13 +3246,14 @@ var COMMANDS = [
|
|
|
2796
3246
|
section: "this machine",
|
|
2797
3247
|
summary: "install or repair the local Mend server",
|
|
2798
3248
|
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]"
|
|
3249
|
+
"[--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>...] [--npm-mirror | --no-npm-mirror] [--npm-mirror-max-size <size>] [--docker-mirror | --no-docker-mirror] [--docker-mirror-max-size <size>] [--docker-hub-username <name> --docker-hub-token-stdin --docker-hub-public-only | --no-docker-hub-login] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
|
|
2800
3250
|
],
|
|
2801
3251
|
description: [
|
|
2802
3252
|
"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.",
|
|
2803
3253
|
"The default listens only on localhost at http://localhost:3105. Non-local access requires both --bind and --url. Every extra browser origin must be named with --origin; setup never guesses from the request Host header or network interfaces.",
|
|
2804
3254
|
"--edge <host> runs a TLS edge in front of Mend: Caddy on ports 80 and 443 of every interface, which obtains and renews a certificate for the host and proxies to Mend's web tier. Mend's own port stays on loopback and the browser origin is https://<host>. The edge's compose overlay and Caddyfile are written into the generation beside compose.yaml, so start, restart and upgrade run them every time. --no-edge takes it away again, and the edge's container with it. A fresh install cannot start with the edge: until the first account exists, registration is open to whoever reaches the origin first, so set up on localhost, create the account, then add the edge.",
|
|
2805
3255
|
"--exposure declares how the instance is reached, and --tenancy whether one organization or many use it. Both are written into the generation and kept across reruns and upgrades. public needs the edge and an existing first account. With multi, or with public, the multi mode gate's settings follow: MEND_SOURCE_POLICY=tenant and MEND_CAPTURE_REQUIRE_SIZES=true, and public sets MEND_URL_BEARERS=refuse. The server still decides whether it starts, and mend server status shows what it reports.",
|
|
3256
|
+
"Two mirrors run beside Mend unless turned off: an npm mirror (nginx caching npmjs.org, capped at 10g by default, least recently used out) and a Docker mirror (a pull-through cache of Docker Hub, capped at 20g by default: over the cap its cache is cleared). Both leave 5g free on their disk: below that, nginx evicts and the Docker mirror pauses, and sessions pull from Docker Hub directly. Neither publishes a host port. Sessions install npm packages and pull Docker Hub images through them; a project's own npm settings win, and a mirror that is down sends sessions upstream instead. Both are kept across reruns and upgrades, and an upgrade adds them to an install from before them. The Docker mirror pulls anonymously unless given a Docker Hub login: --docker-hub-username with the access token piped on standard input, kept in server.env only. The mirror has no login of its own, so every session that reaches it can pull whatever that token can read: setup takes a login only with --docker-hub-public-only, your statement that the token's access permission is Public Repo Read-only. Mend cannot check a token's scope.",
|
|
2806
3257
|
"Docker Desktop on Linux and macOS, and OrbStack on macOS, expose client-side proxy sockets. Containers use the daemon-side /var/run/docker.sock. --docker-socket overrides detection and is retained on reruns.",
|
|
2807
3258
|
"Setup holds an exclusive process lock through startup and health checks. A busy lock reports its owner and manual recovery steps. Never remove a live lock. Private configuration uses immutable generations and an atomic active pointer; failed attempts retain credentials and never delete Docker volumes."
|
|
2808
3259
|
],
|
|
@@ -2816,6 +3267,10 @@ var COMMANDS = [
|
|
|
2816
3267
|
text: "exact Mend server version, or latest; omitted keeps the current pin"
|
|
2817
3268
|
},
|
|
2818
3269
|
{ flag: "--bind <ip>", text: "published listen address. Default: 127.0.0.1" },
|
|
3270
|
+
{
|
|
3271
|
+
flag: "--ssh-bind <ip>",
|
|
3272
|
+
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"
|
|
3273
|
+
},
|
|
2819
3274
|
{ flag: "--url <origin>", text: "advertised browser URL, required with a non-loopback bind" },
|
|
2820
3275
|
{
|
|
2821
3276
|
flag: "--origin <origin>",
|
|
@@ -2839,6 +3294,39 @@ var COMMANDS = [
|
|
|
2839
3294
|
flag: "--tenancy <v>",
|
|
2840
3295
|
text: "declare single or multi; kept across reruns and upgrades. multi also sets the multi mode gate's variables"
|
|
2841
3296
|
},
|
|
3297
|
+
{
|
|
3298
|
+
flag: "--declare <item>",
|
|
3299
|
+
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"
|
|
3300
|
+
},
|
|
3301
|
+
{
|
|
3302
|
+
flag: "--npm-mirror, --no-npm-mirror",
|
|
3303
|
+
text: "run or stop the npm mirror; kept across reruns and upgrades. Default: on"
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
flag: "--npm-mirror-max-size <size>",
|
|
3307
|
+
text: "the npm mirror's disk cap, such as 20g or 1536m, at least 1g. Default: 10g"
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
flag: "--docker-mirror, --no-docker-mirror",
|
|
3311
|
+
text: "run or stop the Docker Hub mirror; kept across reruns and upgrades. Default: on"
|
|
3312
|
+
},
|
|
3313
|
+
{
|
|
3314
|
+
flag: "--docker-mirror-max-size <size>",
|
|
3315
|
+
text: "the Docker mirror's cap, such as 40g; over it the cache is cleared and fills again. Default: 20g"
|
|
3316
|
+
},
|
|
3317
|
+
{
|
|
3318
|
+
flag: "--docker-hub-username <name>",
|
|
3319
|
+
text: "the Docker mirror pulls as this Docker Hub account; needs --docker-hub-token-stdin"
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
flag: "--docker-hub-token-stdin",
|
|
3323
|
+
text: "read that account's access token from standard input; kept in server.env, never in argv or a session"
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
flag: "--docker-hub-public-only",
|
|
3327
|
+
text: "required with a login: you state the token is scoped Public Repo Read-only, since every session can pull what it can read"
|
|
3328
|
+
},
|
|
3329
|
+
{ flag: "--no-docker-hub-login", text: "the Docker mirror pulls anonymously again" },
|
|
2842
3330
|
{
|
|
2843
3331
|
flag: "--assets-dir <dir>",
|
|
2844
3332
|
text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
|
|
@@ -2859,6 +3347,10 @@ var COMMANDS = [
|
|
|
2859
3347
|
command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
|
|
2860
3348
|
text: "install from local release assets and preloaded images"
|
|
2861
3349
|
},
|
|
3350
|
+
{
|
|
3351
|
+
command: "mend server setup --edge mend.example.com --ssh-bind 0.0.0.0 --exposure public --declare workspace-ssh",
|
|
3352
|
+
text: "the TLS edge for the browser and the API, and workspace SSH for Remote-SSH from other machines"
|
|
3353
|
+
},
|
|
2862
3354
|
{
|
|
2863
3355
|
command: "mend server setup --edge mend.example.com",
|
|
2864
3356
|
text: "a TLS edge for mend.example.com; its DNS points at this machine and 80 and 443 reach it"
|
|
@@ -2866,6 +3358,10 @@ var COMMANDS = [
|
|
|
2866
3358
|
{
|
|
2867
3359
|
command: "mend server setup --exposure public --tenancy multi",
|
|
2868
3360
|
text: "on an install with an edge and a first account: declare public, many organizations"
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
command: 'printf %s "$DOCKER_HUB_TOKEN" | mend server setup --docker-hub-username mendbot --docker-hub-token-stdin --docker-hub-public-only',
|
|
3364
|
+
text: "the Docker mirror pulls from Docker Hub as mendbot, with a Public Repo Read-only token"
|
|
2869
3365
|
}
|
|
2870
3366
|
],
|
|
2871
3367
|
see: ["server", "server status", "login", "doctor", "operator exposure"]
|
|
@@ -2873,10 +3369,11 @@ var COMMANDS = [
|
|
|
2873
3369
|
{
|
|
2874
3370
|
name: "server status",
|
|
2875
3371
|
section: "this machine",
|
|
2876
|
-
summary: "show the pin, generation, containers, edge and posture",
|
|
3372
|
+
summary: "show the pin, generation, containers, edge, mirrors and posture",
|
|
2877
3373
|
synopsis: [""],
|
|
2878
3374
|
description: [
|
|
2879
3375
|
"Reads the existing installation without changing its files. A running Mend must answer health with the exact pinned version. A stopped server makes no health claim. Never installs a server implicitly.",
|
|
3376
|
+
"Each mirror the install runs follows, as observed: whether its container runs, what its cache holds, and its traffic. For the npm mirror, the tarball requests in its log for the last 24 hours and how many the cache served; for the Docker mirror, layer and manifest requests since it started and how many the cache served.",
|
|
2880
3377
|
"Then the posture, declared beside observed. Declared is what this install's configuration says: the edge host, MEND_EXPOSURE and MEND_TENANCY, a default named as one. Observed is what was seen: whether the edge's container runs and whether Caddy's data holds a certificate for the host, and what the running server reports in its health, the exposure it runs with and how many public exposure gate items are open, the tenancy and which multi mode gate items are open. When this machine is signed in to the install as the operator, every item of both gates follows with its detail, as mend operator gate and mend operator exposure print them. None of it is a verdict: the report says what was declared and what was observed."
|
|
2881
3378
|
],
|
|
2882
3379
|
see: ["server logs", "server start", "operator gate", "operator exposure"]
|
|
@@ -2895,7 +3392,7 @@ var COMMANDS = [
|
|
|
2895
3392
|
{
|
|
2896
3393
|
name: "server stop",
|
|
2897
3394
|
section: "this machine",
|
|
2898
|
-
summary: "stop
|
|
3395
|
+
summary: "stop the server's containers without deleting data",
|
|
2899
3396
|
synopsis: [""],
|
|
2900
3397
|
description: [
|
|
2901
3398
|
"Stops only the installation's Compose services, the edge among them when one is set. Connections are interrupted. Workspace containers and volumes remain, but active work may lose connectivity and need reconnection. No volume deletion or Docker prune is performed."
|
|
@@ -3424,7 +3921,7 @@ var writeBundle = (files, outPath, now) => {
|
|
|
3424
3921
|
entries: entries.map((entry) => ({ path: entry.path, bytes: entry.content.length }))
|
|
3425
3922
|
};
|
|
3426
3923
|
};
|
|
3427
|
-
var
|
|
3924
|
+
var formatBytes2 = (bytes) => {
|
|
3428
3925
|
if (bytes < 1024) return `${bytes} B`;
|
|
3429
3926
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
3430
3927
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -3433,8 +3930,8 @@ var NOTICE = "Contains logs and configuration; secrets are redacted, but read it
|
|
|
3433
3930
|
var bundleReport = (written) => {
|
|
3434
3931
|
const width = Math.max(...written.entries.map((entry) => entry.path.length), 0);
|
|
3435
3932
|
return [
|
|
3436
|
-
`${written.path} \xB7 ${
|
|
3437
|
-
...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${
|
|
3933
|
+
`${written.path} \xB7 ${formatBytes2(written.bytes)}`,
|
|
3934
|
+
...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${formatBytes2(entry.bytes)}`),
|
|
3438
3935
|
NOTICE
|
|
3439
3936
|
];
|
|
3440
3937
|
};
|
|
@@ -4256,7 +4753,7 @@ var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.
|
|
|
4256
4753
|
|
|
4257
4754
|
// src/http-client.ts
|
|
4258
4755
|
import { Agent, setGlobalDispatcher } from "undici";
|
|
4259
|
-
var cliDispatcher = (
|
|
4756
|
+
var cliDispatcher = (connect3) => new Agent({ allowH2: false, ...connect3 === void 0 ? {} : { connect: connect3 } });
|
|
4260
4757
|
var useHttp1 = () => {
|
|
4261
4758
|
setGlobalDispatcher(cliDispatcher());
|
|
4262
4759
|
};
|
|
@@ -4579,7 +5076,7 @@ var fetchedLines = (fetched) => {
|
|
|
4579
5076
|
lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
|
|
4580
5077
|
return lines;
|
|
4581
5078
|
};
|
|
4582
|
-
var
|
|
5079
|
+
var formatBytes3 = (bytes) => {
|
|
4583
5080
|
if (bytes < 1024) return `${bytes} bytes`;
|
|
4584
5081
|
const units = ["KiB", "MiB", "GiB"];
|
|
4585
5082
|
let value = bytes / 1024;
|
|
@@ -4612,7 +5109,7 @@ var bundleRefusal = (downloaded) => {
|
|
|
4612
5109
|
const size = numberField(body, "size");
|
|
4613
5110
|
const limit = numberField(body, "limit");
|
|
4614
5111
|
if (downloaded.status === 413 && size !== null && limit !== null) {
|
|
4615
|
-
return `bundle not sent \xB7 ${
|
|
5112
|
+
return `bundle not sent \xB7 ${formatBytes3(size)} (${size} bytes) \xB7 the server's limit is ${formatBytes3(limit)} (MEND_BUDGET_BUNDLE_BYTES) \xB7 nothing was fetched`;
|
|
4616
5113
|
}
|
|
4617
5114
|
return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
|
|
4618
5115
|
};
|
|
@@ -5241,13 +5738,13 @@ var renderExposure = (report) => {
|
|
|
5241
5738
|
const mark = (item) => marks[item.established];
|
|
5242
5739
|
const open = report.items.filter((item) => item.established === "open");
|
|
5243
5740
|
const blocking = open.filter((item) => item.blocksStart).length;
|
|
5244
|
-
const
|
|
5741
|
+
const notBlocking = open.length - blocking;
|
|
5245
5742
|
return [
|
|
5246
5743
|
`exposure \xB7 declared ${report.declared} \xB7 ${EXPOSURE_WORDS[report.declared]}`,
|
|
5247
5744
|
...report.items.map(
|
|
5248
5745
|
(item) => `${mark(item)} ${item.id.padEnd(width)} ${item.established.padEnd(8)} ${item.detail}${item.fix === null ? "" : dim4(` \xB7 ${item.fix}`)}`
|
|
5249
5746
|
),
|
|
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}
|
|
5747
|
+
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
5748
|
];
|
|
5252
5749
|
};
|
|
5253
5750
|
var renderOrganizations = (rows) => {
|
|
@@ -5644,21 +6141,39 @@ var piProfileLines = (scan) => {
|
|
|
5644
6141
|
|
|
5645
6142
|
// src/run-scripts.ts
|
|
5646
6143
|
var RUN_ARGV_MAX = 64;
|
|
6144
|
+
var RUN_ARGV_MAX_WORD_BYTES = 128 * 1024 - 1;
|
|
6145
|
+
var RUN_ARGV_MAX_TOTAL_BYTES = 1024 * 1024;
|
|
5647
6146
|
var whitespaceName = (character) => character === "\n" || character === "\r" ? "a newline" : character === " " ? "a tab" : character === " " ? "a space" : "whitespace";
|
|
6147
|
+
var LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
6148
|
+
var utf8Bytes = (value) => Buffer.byteLength(value, "utf8");
|
|
5648
6149
|
var runArgvIssue = (argv) => {
|
|
5649
6150
|
if (argv.length > RUN_ARGV_MAX) {
|
|
5650
6151
|
return `the command has ${argv.length} words and the platform takes at most ${RUN_ARGV_MAX} \xB7 put them in a script and run that`;
|
|
5651
6152
|
}
|
|
6153
|
+
const program = argv[0] ?? "";
|
|
6154
|
+
if (program === "") return "the program is empty";
|
|
6155
|
+
if (program.trim() !== program) {
|
|
6156
|
+
const leading = program.trimStart() !== program;
|
|
6157
|
+
const edge = leading ? program.charAt(0) : program.charAt(program.length - 1);
|
|
6158
|
+
return `the program ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 trim it and run again`;
|
|
6159
|
+
}
|
|
6160
|
+
let total = 0;
|
|
5652
6161
|
for (const [index, word] of argv.entries()) {
|
|
5653
6162
|
const which = index === 0 ? "the program" : `argument ${index}`;
|
|
5654
|
-
if (word
|
|
5655
|
-
return `${which}
|
|
6163
|
+
if (word.includes("\0")) {
|
|
6164
|
+
return `${which} contains a NUL byte, which no process argument can carry`;
|
|
6165
|
+
}
|
|
6166
|
+
if (LONE_SURROGATE.test(word)) {
|
|
6167
|
+
return `${which} is not well-formed Unicode (a lone surrogate)`;
|
|
5656
6168
|
}
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
return `${which} ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 the platform refuses arguments with leading or trailing whitespace \xB7 trim it and run again`;
|
|
6169
|
+
const bytes = utf8Bytes(word);
|
|
6170
|
+
if (bytes > RUN_ARGV_MAX_WORD_BYTES) {
|
|
6171
|
+
return `${which} is ${bytes} bytes and the platform takes at most ${RUN_ARGV_MAX_WORD_BYTES} a word \xB7 put it in a script and run that`;
|
|
5661
6172
|
}
|
|
6173
|
+
total += bytes;
|
|
6174
|
+
}
|
|
6175
|
+
if (total > RUN_ARGV_MAX_TOTAL_BYTES) {
|
|
6176
|
+
return `the command is ${total} bytes and the platform takes at most ${RUN_ARGV_MAX_TOTAL_BYTES} \xB7 put it in a script and run that`;
|
|
5662
6177
|
}
|
|
5663
6178
|
return null;
|
|
5664
6179
|
};
|
|
@@ -5811,16 +6326,22 @@ var splitArgs = (args, valued, switches) => {
|
|
|
5811
6326
|
return { values, on, session };
|
|
5812
6327
|
};
|
|
5813
6328
|
var parseLogsArgs = (args) => {
|
|
5814
|
-
const split = splitArgs(args, ["--from", "--process"], ["--follow", "-f"]);
|
|
6329
|
+
const split = splitArgs(args, ["--from", "--process", "--service"], ["--follow", "-f"]);
|
|
5815
6330
|
if ("error" in split) return split;
|
|
5816
6331
|
const from = split.values.get("--from") ?? "0";
|
|
5817
6332
|
if (!/^(0|[1-9]\d*)$/u.test(from)) return { error: "--from takes a record sequence, e.g. 0" };
|
|
6333
|
+
const process2 = split.values.get("--process") ?? null;
|
|
6334
|
+
const service = split.values.get("--service") ?? null;
|
|
6335
|
+
if (process2 !== null && service !== null) {
|
|
6336
|
+
return { error: "--process and --service each name one process; pass one of them" };
|
|
6337
|
+
}
|
|
5818
6338
|
return {
|
|
5819
6339
|
args: {
|
|
5820
6340
|
session: split.session,
|
|
5821
6341
|
follow: split.on.has("--follow") || split.on.has("-f"),
|
|
5822
6342
|
from,
|
|
5823
|
-
process:
|
|
6343
|
+
process: process2,
|
|
6344
|
+
service
|
|
5824
6345
|
}
|
|
5825
6346
|
};
|
|
5826
6347
|
};
|
|
@@ -5828,21 +6349,149 @@ var parseWaitArgs = (args) => {
|
|
|
5828
6349
|
const split = splitArgs(args, ["--timeout", "--process"], ["--json"]);
|
|
5829
6350
|
if ("error" in split) return split;
|
|
5830
6351
|
const timeout = split.values.get("--timeout");
|
|
5831
|
-
const
|
|
5832
|
-
if (
|
|
5833
|
-
return { error: "--timeout takes a number of seconds above 0" };
|
|
5834
|
-
}
|
|
6352
|
+
const timeoutMs = timeout === void 0 ? null : parseDuration(timeout);
|
|
6353
|
+
if (timeoutMs === null && timeout !== void 0) return { error: DURATION_ISSUE };
|
|
5835
6354
|
return {
|
|
5836
6355
|
args: {
|
|
5837
6356
|
session: split.session,
|
|
5838
6357
|
process: split.values.get("--process") ?? null,
|
|
5839
|
-
timeoutMs
|
|
6358
|
+
timeoutMs,
|
|
5840
6359
|
json: split.on.has("--json")
|
|
5841
6360
|
}
|
|
5842
6361
|
};
|
|
5843
6362
|
};
|
|
6363
|
+
var DURATION_UNIT_MS = { s: 1e3, m: 6e4, h: 36e5 };
|
|
6364
|
+
var parseDuration = (text) => {
|
|
6365
|
+
const trimmed = text.trim();
|
|
6366
|
+
const unit = DURATION_UNIT_MS[trimmed.slice(-1)];
|
|
6367
|
+
const amount = unit === void 0 ? trimmed === "" ? Number.NaN : Number(trimmed) : /^(?:\d+\.?\d*|\.\d+)$/u.test(trimmed.slice(0, -1)) ? Number(trimmed.slice(0, -1)) : Number.NaN;
|
|
6368
|
+
const ms = Math.ceil(amount * (unit ?? 1e3));
|
|
6369
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
6370
|
+
};
|
|
6371
|
+
var DURATION_ISSUE = "--timeout takes a duration above 0: 90 or 90s, 5m, 1h";
|
|
6372
|
+
var durationLine = (ms) => {
|
|
6373
|
+
const seconds2 = Math.round(ms / 1e3);
|
|
6374
|
+
if (seconds2 < 60) return `${seconds2} s`;
|
|
6375
|
+
const minutes = Math.floor(seconds2 / 60);
|
|
6376
|
+
const rest = seconds2 % 60;
|
|
6377
|
+
if (minutes < 60) return rest === 0 ? `${minutes} min` : `${minutes} min ${rest} s`;
|
|
6378
|
+
const hours = Math.floor(minutes / 60);
|
|
6379
|
+
return minutes % 60 === 0 ? `${hours} h` : `${hours} h ${minutes % 60} min`;
|
|
6380
|
+
};
|
|
5844
6381
|
var WAIT_TIMED_OUT = 124;
|
|
5845
6382
|
var isSessionId = (word) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(word);
|
|
6383
|
+
var SERVICE_PROCESS_ENDED = 2;
|
|
6384
|
+
var SERVICE_WORKSPACE_ENDED = 3;
|
|
6385
|
+
var begunBy = (view, attempt2, startId) => attempt2.launchCorrelationId === serviceStartCorrelation(view.service.id, startId);
|
|
6386
|
+
var findStartAttempt = (services, target) => {
|
|
6387
|
+
for (const view of services) {
|
|
6388
|
+
if (view.service.sessionId !== target.sessionId) continue;
|
|
6389
|
+
const attempt2 = view.attempts.find(
|
|
6390
|
+
(candidate) => target.attemptId !== null ? candidate.id === target.attemptId : begunBy(view, candidate, target.startId)
|
|
6391
|
+
);
|
|
6392
|
+
if (attempt2 !== void 0) return { view, attempt: attempt2 };
|
|
6393
|
+
}
|
|
6394
|
+
return void 0;
|
|
6395
|
+
};
|
|
6396
|
+
var STAMP_KIND = serviceStartCorrelation("", "").split(":")[0] ?? "service-start";
|
|
6397
|
+
var answeredAttemptId = (view, startId) => {
|
|
6398
|
+
const own = view.attempts.find((attempt2) => begunBy(view, attempt2, startId));
|
|
6399
|
+
if (own !== void 0) return own.id;
|
|
6400
|
+
const stamps = view.attempts.some(
|
|
6401
|
+
(attempt2) => attempt2.launchCorrelationId?.startsWith(`${STAMP_KIND}:`) === true
|
|
6402
|
+
);
|
|
6403
|
+
return stamps ? null : view.service.currentAttemptId;
|
|
6404
|
+
};
|
|
6405
|
+
var timeOf = (iso) => {
|
|
6406
|
+
if (iso === void 0) return null;
|
|
6407
|
+
const at = Date.parse(iso);
|
|
6408
|
+
return Number.isFinite(at) ? at : null;
|
|
6409
|
+
};
|
|
6410
|
+
var answeredFor = (view, attempt2) => {
|
|
6411
|
+
const observation = view.latestObservation;
|
|
6412
|
+
if (view.service.currentAttemptId !== attempt2.id || view.currentForward === null) return false;
|
|
6413
|
+
if (observation === null || observation.forwardId !== view.currentForward.id) return false;
|
|
6414
|
+
if (observation.state !== "reachable") return false;
|
|
6415
|
+
const began = timeOf(attempt2.createdAt);
|
|
6416
|
+
const observed = timeOf(observation.lastObservedAt);
|
|
6417
|
+
return began === null || observed === null || observed >= began;
|
|
6418
|
+
};
|
|
6419
|
+
var serviceStartStateOf = (read, target) => {
|
|
6420
|
+
const found = findStartAttempt(read.services, target);
|
|
6421
|
+
if (found !== void 0 && found.attempt.exitedAt !== null) {
|
|
6422
|
+
return {
|
|
6423
|
+
kind: "process-ended",
|
|
6424
|
+
processId: found.attempt.id,
|
|
6425
|
+
status: found.attempt.status,
|
|
6426
|
+
exitCode: found.attempt.exitCode ?? null
|
|
6427
|
+
};
|
|
6428
|
+
}
|
|
6429
|
+
if (read.sessionStatus === null) return { kind: "workspace-ended", sessionStatus: null };
|
|
6430
|
+
if (found === void 0) return { kind: "starting", processId: target.attemptId };
|
|
6431
|
+
return answeredFor(found.view, found.attempt) ? { kind: "answered", processId: found.attempt.id, view: found.view } : { kind: "starting", processId: found.attempt.id };
|
|
6432
|
+
};
|
|
6433
|
+
var waitForServiceStart = async (options) => {
|
|
6434
|
+
const pollMs = options.pollMs ?? 2e3;
|
|
6435
|
+
const discoverWithinMs = options.unreachableAfterMs ?? 6e4;
|
|
6436
|
+
const since = options.now();
|
|
6437
|
+
let target = options.target;
|
|
6438
|
+
let last = null;
|
|
6439
|
+
for (; ; ) {
|
|
6440
|
+
const got = await readThrough(options.read, options, options.deadline);
|
|
6441
|
+
if (!got.done) return { kind: "timeout", last };
|
|
6442
|
+
const state = serviceStartStateOf(got.value, target);
|
|
6443
|
+
if (state.kind !== "starting") return state;
|
|
6444
|
+
last = state;
|
|
6445
|
+
if (state.processId === null) {
|
|
6446
|
+
if (options.now() - since >= discoverWithinMs) return { kind: "no-attempt" };
|
|
6447
|
+
} else if (target.attemptId === null) {
|
|
6448
|
+
target = { ...target, attemptId: state.processId };
|
|
6449
|
+
}
|
|
6450
|
+
options.onStarting?.(state);
|
|
6451
|
+
const left = options.deadline === null ? pollMs : Math.min(pollMs, options.deadline - options.now());
|
|
6452
|
+
if (left <= 0) return { kind: "timeout", last };
|
|
6453
|
+
await options.sleep(left);
|
|
6454
|
+
}
|
|
6455
|
+
};
|
|
6456
|
+
var readWithin = (read, clock2, deadline) => readThrough(read, clock2, deadline);
|
|
6457
|
+
var candidateLine = (view) => `${view.service.id} (session ${view.service.sessionId.slice(0, 8)})`;
|
|
6458
|
+
var pickServiceAttempt = (services, needle, sessionId) => {
|
|
6459
|
+
const inScope = services.filter(
|
|
6460
|
+
(view) => sessionId === null || view.service.sessionId === sessionId
|
|
6461
|
+
);
|
|
6462
|
+
const byId = inScope.filter((view) => view.service.id === needle);
|
|
6463
|
+
const byName = inScope.filter((view) => view.service.name === needle);
|
|
6464
|
+
const matches = byId.length > 0 ? byId : byName.length > 0 ? byName : inScope.filter((view) => view.service.id.startsWith(needle));
|
|
6465
|
+
const [chosen] = matches;
|
|
6466
|
+
if (chosen === void 0) {
|
|
6467
|
+
return {
|
|
6468
|
+
error: `no Service ${sessionId === null ? "" : "of this session "}is named "${needle}" or has an id starting with it`,
|
|
6469
|
+
named: false
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
if (matches.length > 1) {
|
|
6473
|
+
return {
|
|
6474
|
+
error: `"${needle}" names ${matches.length} Services \xB7 name one by its id: ${matches.map(candidateLine).join(", ")}`,
|
|
6475
|
+
named: true
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6478
|
+
const name = chosen.service.name;
|
|
6479
|
+
const attemptId = chosen.service.currentAttemptId;
|
|
6480
|
+
if (attemptId === null) {
|
|
6481
|
+
return {
|
|
6482
|
+
error: `Service ${name} has no attempt yet: Mend has run no process for it, so nothing is recorded (an adopted port has none; mend service run starts one)`,
|
|
6483
|
+
named: true
|
|
6484
|
+
};
|
|
6485
|
+
}
|
|
6486
|
+
const attempt2 = chosen.attempts.find((candidate) => candidate.id === attemptId);
|
|
6487
|
+
if (attempt2 === void 0 || attempt2.sealantSessionId === null) {
|
|
6488
|
+
return {
|
|
6489
|
+
error: `Service ${name}'s attempt ${attemptId.slice(0, 8)} has not opened its terminal yet, so nothing is recorded \xB7 try again in a moment`,
|
|
6490
|
+
named: true
|
|
6491
|
+
};
|
|
6492
|
+
}
|
|
6493
|
+
return { service: chosen, processId: attemptId };
|
|
6494
|
+
};
|
|
5846
6495
|
|
|
5847
6496
|
// src/secret-files.ts
|
|
5848
6497
|
import * as path14 from "node:path";
|
|
@@ -6148,7 +6797,14 @@ var writeDurable = (file, content, mode = 384) => {
|
|
|
6148
6797
|
}
|
|
6149
6798
|
};
|
|
6150
6799
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
6151
|
-
var optionalFileKeys = [
|
|
6800
|
+
var optionalFileKeys = [
|
|
6801
|
+
"posture",
|
|
6802
|
+
"edge",
|
|
6803
|
+
"caddyfile",
|
|
6804
|
+
"mirrors",
|
|
6805
|
+
"npmMirrorConf",
|
|
6806
|
+
"dockerMirrorGuard"
|
|
6807
|
+
];
|
|
6152
6808
|
var fileNames = {
|
|
6153
6809
|
identity: "identity.env",
|
|
6154
6810
|
config: "server.json",
|
|
@@ -6157,7 +6813,10 @@ var fileNames = {
|
|
|
6157
6813
|
postgresInit: "postgres-init.sh",
|
|
6158
6814
|
posture: "compose.posture.yaml",
|
|
6159
6815
|
edge: "compose.edge.yaml",
|
|
6160
|
-
caddyfile: "Caddyfile"
|
|
6816
|
+
caddyfile: "Caddyfile",
|
|
6817
|
+
mirrors: "compose.mirrors.yaml",
|
|
6818
|
+
npmMirrorConf: "npm-mirror.conf",
|
|
6819
|
+
dockerMirrorGuard: "docker-mirror-guard.sh"
|
|
6161
6820
|
};
|
|
6162
6821
|
var fileModes = {
|
|
6163
6822
|
identity: 384,
|
|
@@ -6167,7 +6826,10 @@ var fileModes = {
|
|
|
6167
6826
|
postgresInit: 493,
|
|
6168
6827
|
posture: 384,
|
|
6169
6828
|
edge: 384,
|
|
6170
|
-
caddyfile: 420
|
|
6829
|
+
caddyfile: 420,
|
|
6830
|
+
mirrors: 384,
|
|
6831
|
+
npmMirrorConf: 420,
|
|
6832
|
+
dockerMirrorGuard: 420
|
|
6171
6833
|
};
|
|
6172
6834
|
var allFiles = (files) => [...fileKeys, ...optionalFileKeys].map((key) => [key, files[key]]);
|
|
6173
6835
|
var parseLockOwner = (raw) => {
|
|
@@ -6271,6 +6933,9 @@ var readActive = (paths) => {
|
|
|
6271
6933
|
const posture = optional("posture");
|
|
6272
6934
|
const edge = optional("edge");
|
|
6273
6935
|
const caddyfile = optional("caddyfile");
|
|
6936
|
+
const mirrors = optional("mirrors");
|
|
6937
|
+
const npmMirrorConf = optional("npmMirrorConf");
|
|
6938
|
+
const dockerMirrorGuard = optional("dockerMirrorGuard");
|
|
6274
6939
|
const files = {
|
|
6275
6940
|
identity: fs14.readFileSync(path15.join(directory, fileNames.identity), "utf8"),
|
|
6276
6941
|
config: fs14.readFileSync(path15.join(directory, fileNames.config), "utf8"),
|
|
@@ -6279,7 +6944,10 @@ var readActive = (paths) => {
|
|
|
6279
6944
|
postgresInit: fs14.readFileSync(path15.join(directory, fileNames.postgresInit), "utf8"),
|
|
6280
6945
|
...posture === void 0 ? {} : { posture },
|
|
6281
6946
|
...edge === void 0 ? {} : { edge },
|
|
6282
|
-
...caddyfile === void 0 ? {} : { caddyfile }
|
|
6947
|
+
...caddyfile === void 0 ? {} : { caddyfile },
|
|
6948
|
+
...mirrors === void 0 ? {} : { mirrors },
|
|
6949
|
+
...npmMirrorConf === void 0 ? {} : { npmMirrorConf },
|
|
6950
|
+
...dockerMirrorGuard === void 0 ? {} : { dockerMirrorGuard }
|
|
6283
6951
|
};
|
|
6284
6952
|
if (readIdentity(paths) !== files.identity) {
|
|
6285
6953
|
throw new ServerStoreError(
|
|
@@ -6817,6 +7485,7 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6817
7485
|
"--context",
|
|
6818
7486
|
"--version",
|
|
6819
7487
|
"--bind",
|
|
7488
|
+
"--ssh-bind",
|
|
6820
7489
|
"--url",
|
|
6821
7490
|
"--origin",
|
|
6822
7491
|
"--port",
|
|
@@ -6827,7 +7496,29 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6827
7496
|
"--edge",
|
|
6828
7497
|
"--no-edge",
|
|
6829
7498
|
"--exposure",
|
|
6830
|
-
"--tenancy"
|
|
7499
|
+
"--tenancy",
|
|
7500
|
+
"--declare",
|
|
7501
|
+
"--npm-mirror",
|
|
7502
|
+
"--no-npm-mirror",
|
|
7503
|
+
"--npm-mirror-max-size",
|
|
7504
|
+
"--docker-mirror",
|
|
7505
|
+
"--no-docker-mirror",
|
|
7506
|
+
"--docker-mirror-max-size",
|
|
7507
|
+
"--docker-hub-username",
|
|
7508
|
+
"--docker-hub-token-stdin",
|
|
7509
|
+
"--docker-hub-public-only",
|
|
7510
|
+
"--no-docker-hub-login"
|
|
7511
|
+
]);
|
|
7512
|
+
var SWITCHES = /* @__PURE__ */ new Set([
|
|
7513
|
+
"--offline",
|
|
7514
|
+
"--no-edge",
|
|
7515
|
+
"--npm-mirror",
|
|
7516
|
+
"--no-npm-mirror",
|
|
7517
|
+
"--docker-mirror",
|
|
7518
|
+
"--no-docker-mirror",
|
|
7519
|
+
"--docker-hub-token-stdin",
|
|
7520
|
+
"--docker-hub-public-only",
|
|
7521
|
+
"--no-docker-hub-login"
|
|
6831
7522
|
]);
|
|
6832
7523
|
var parseExposure = (value) => {
|
|
6833
7524
|
if (!isExposure(value))
|
|
@@ -6839,6 +7530,21 @@ var parseTenancy = (value) => {
|
|
|
6839
7530
|
throw setupError(`--tenancy must be one of ${TENANCIES.join(", ")}, not "${value}".`);
|
|
6840
7531
|
return value;
|
|
6841
7532
|
};
|
|
7533
|
+
var parseDeclared = (values) => {
|
|
7534
|
+
if (values.length === 1 && values[0] === "none") return [];
|
|
7535
|
+
return [
|
|
7536
|
+
...new Set(
|
|
7537
|
+
values.map((value) => {
|
|
7538
|
+
if (!isDeclarableItem(value)) {
|
|
7539
|
+
throw setupError(
|
|
7540
|
+
`--declare takes ${DECLARABLE_ITEMS.join(", ")} or none, not "${value}". Every other gate item is observed by the server, never stated.`
|
|
7541
|
+
);
|
|
7542
|
+
}
|
|
7543
|
+
return value;
|
|
7544
|
+
})
|
|
7545
|
+
)
|
|
7546
|
+
];
|
|
7547
|
+
};
|
|
6842
7548
|
var parseEdge = (value) => {
|
|
6843
7549
|
const host = parseEdgeHost(value);
|
|
6844
7550
|
if (host === null) {
|
|
@@ -6855,7 +7561,7 @@ var parseSetupOptions = (args) => {
|
|
|
6855
7561
|
if (flag === void 0) continue;
|
|
6856
7562
|
if (!flag.startsWith("--")) throw setupError(`Unexpected server setup argument "${flag}".`);
|
|
6857
7563
|
if (!SETUP_FLAGS.has(flag)) throw setupError(`Unknown server setup option "${flag}".`);
|
|
6858
|
-
if (flag
|
|
7564
|
+
if (SWITCHES.has(flag)) {
|
|
6859
7565
|
if (values.has(flag)) throw setupError(`${flag} may be supplied only once.`);
|
|
6860
7566
|
values.set(flag, ["true"]);
|
|
6861
7567
|
continue;
|
|
@@ -6863,7 +7569,7 @@ var parseSetupOptions = (args) => {
|
|
|
6863
7569
|
const [value, valueIndex] = nextFlagValue(args, index, flag);
|
|
6864
7570
|
index = valueIndex;
|
|
6865
7571
|
const previous = values.get(flag) ?? [];
|
|
6866
|
-
if (flag !== "--origin" && previous.length > 0) {
|
|
7572
|
+
if (flag !== "--origin" && flag !== "--declare" && previous.length > 0) {
|
|
6867
7573
|
throw setupError(`${flag} may be supplied only once.`);
|
|
6868
7574
|
}
|
|
6869
7575
|
previous.push(value);
|
|
@@ -6877,12 +7583,59 @@ var parseSetupOptions = (args) => {
|
|
|
6877
7583
|
const edge = flagValue3("--edge");
|
|
6878
7584
|
const exposure = flagValue3("--exposure");
|
|
6879
7585
|
const tenancy = flagValue3("--tenancy");
|
|
7586
|
+
const declared = values.get("--declare");
|
|
6880
7587
|
if (edge !== void 0 && values.has("--no-edge"))
|
|
6881
7588
|
throw setupError("--edge and --no-edge contradict each other.");
|
|
7589
|
+
const pair = (on, off) => {
|
|
7590
|
+
if (values.has(on) && values.has(off))
|
|
7591
|
+
throw setupError(`${on} and ${off} contradict each other.`);
|
|
7592
|
+
return values.has(on) ? true : values.has(off) ? false : void 0;
|
|
7593
|
+
};
|
|
7594
|
+
const npmMirror = pair("--npm-mirror", "--no-npm-mirror");
|
|
7595
|
+
const dockerMirror = pair("--docker-mirror", "--no-docker-mirror");
|
|
7596
|
+
const maxSize = flagValue3("--npm-mirror-max-size");
|
|
7597
|
+
const npmMirrorMaxSize = maxSize === void 0 ? void 0 : parseMirrorSize(maxSize);
|
|
7598
|
+
if (npmMirrorMaxSize === null)
|
|
7599
|
+
throw setupError(
|
|
7600
|
+
`--npm-mirror-max-size must be a whole number of gibibytes or mebibytes, at least 1g, such as 20g or 1536m, not "${maxSize ?? ""}".`
|
|
7601
|
+
);
|
|
7602
|
+
if (npmMirrorMaxSize !== void 0 && npmMirror === false)
|
|
7603
|
+
throw setupError("--npm-mirror-max-size and --no-npm-mirror contradict each other.");
|
|
7604
|
+
const dockerMaxSize = flagValue3("--docker-mirror-max-size");
|
|
7605
|
+
const dockerMirrorMaxSize = dockerMaxSize === void 0 ? void 0 : parseMirrorSize(dockerMaxSize);
|
|
7606
|
+
if (dockerMirrorMaxSize === null)
|
|
7607
|
+
throw setupError(
|
|
7608
|
+
`--docker-mirror-max-size must be a whole number of gibibytes or mebibytes, at least 1g, such as 40g, not "${dockerMaxSize ?? ""}".`
|
|
7609
|
+
);
|
|
7610
|
+
if (dockerMirrorMaxSize !== void 0 && dockerMirror === false)
|
|
7611
|
+
throw setupError("--docker-mirror-max-size and --no-docker-mirror contradict each other.");
|
|
7612
|
+
const dockerHubUsername = flagValue3("--docker-hub-username");
|
|
7613
|
+
const dockerHubTokenStdin = values.has("--docker-hub-token-stdin");
|
|
7614
|
+
const noDockerHubLogin = values.has("--no-docker-hub-login");
|
|
7615
|
+
if (dockerHubUsername === void 0 !== !dockerHubTokenStdin)
|
|
7616
|
+
throw setupError(
|
|
7617
|
+
"--docker-hub-username and --docker-hub-token-stdin go together: the user name, and the access token on standard input."
|
|
7618
|
+
);
|
|
7619
|
+
if (dockerHubUsername !== void 0 && !isDockerHubCredential(dockerHubUsername))
|
|
7620
|
+
throw setupError(`--docker-hub-username "${dockerHubUsername}" is not a Docker Hub user name.`);
|
|
7621
|
+
const dockerHubPublicOnly = values.has("--docker-hub-public-only");
|
|
7622
|
+
if (dockerHubPublicOnly && dockerHubUsername === void 0)
|
|
7623
|
+
throw setupError(
|
|
7624
|
+
"--docker-hub-public-only goes with --docker-hub-username and --docker-hub-token-stdin: it states what the token you give can read."
|
|
7625
|
+
);
|
|
7626
|
+
if (dockerHubUsername !== void 0 && !dockerHubPublicOnly)
|
|
7627
|
+
throw setupError(
|
|
7628
|
+
`A Docker Hub login needs --docker-hub-public-only. The Docker mirror has no login of its own: every session that reaches it can pull whatever the token can read, private repositories included. Create a Docker Hub personal access token with the access permission "Public Repo Read-only", pipe it on standard input, and add --docker-hub-public-only to state that it is one. Mend cannot check a token's scope.`
|
|
7629
|
+
);
|
|
7630
|
+
if (dockerHubUsername !== void 0 && (noDockerHubLogin || dockerMirror === false))
|
|
7631
|
+
throw setupError(
|
|
7632
|
+
`--docker-hub-username and ${noDockerHubLogin ? "--no-docker-hub-login" : "--no-docker-mirror"} contradict each other.`
|
|
7633
|
+
);
|
|
6882
7634
|
return {
|
|
6883
7635
|
context: flagValue3("--context"),
|
|
6884
7636
|
version: version === void 0 ? void 0 : parseVersion(version),
|
|
6885
7637
|
bind: flagValue3("--bind"),
|
|
7638
|
+
sshBind: flagValue3("--ssh-bind"),
|
|
6886
7639
|
url: flagValue3("--url"),
|
|
6887
7640
|
origins: origins === void 0 ? void 0 : origins,
|
|
6888
7641
|
appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
|
|
@@ -6893,9 +7646,61 @@ var parseSetupOptions = (args) => {
|
|
|
6893
7646
|
edge: edge === void 0 ? void 0 : parseEdge(edge),
|
|
6894
7647
|
noEdge: values.has("--no-edge"),
|
|
6895
7648
|
exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
|
|
6896
|
-
tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy)
|
|
7649
|
+
tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy),
|
|
7650
|
+
declared: declared === void 0 ? void 0 : parseDeclared(declared),
|
|
7651
|
+
npmMirror,
|
|
7652
|
+
npmMirrorMaxSize,
|
|
7653
|
+
dockerMirror,
|
|
7654
|
+
dockerMirrorMaxSize,
|
|
7655
|
+
dockerHubUsername,
|
|
7656
|
+
dockerHubTokenStdin,
|
|
7657
|
+
noDockerHubLogin
|
|
7658
|
+
};
|
|
7659
|
+
};
|
|
7660
|
+
var resolveMirrors = (saved, options) => {
|
|
7661
|
+
const base = saved ?? DEFAULT_MIRRORS;
|
|
7662
|
+
const npmOn = options.npmMirror ?? (options.npmMirrorMaxSize !== void 0 || base.npm !== null);
|
|
7663
|
+
const dockerOn = options.dockerMirror ?? (options.dockerHubUsername !== void 0 || options.dockerMirrorMaxSize !== void 0 || base.docker !== null);
|
|
7664
|
+
const upstreamUser = options.dockerHubUsername ?? (options.noDockerHubLogin ? void 0 : dockerMirrorLogin(base.docker));
|
|
7665
|
+
const dockerMaxSize = options.dockerMirrorMaxSize ?? base.docker?.maxSize ?? DEFAULT_DOCKER_MIRROR_MAX_SIZE;
|
|
7666
|
+
return {
|
|
7667
|
+
npm: npmOn ? {
|
|
7668
|
+
maxSize: options.npmMirrorMaxSize ?? base.npm?.maxSize ?? DEFAULT_NPM_MIRROR_MAX_SIZE
|
|
7669
|
+
} : null,
|
|
7670
|
+
docker: !dockerOn ? null : upstreamUser === void 0 ? { maxSize: dockerMaxSize } : { maxSize: dockerMaxSize, upstreamUser, upstreamPublicOnly: true }
|
|
6897
7671
|
};
|
|
6898
7672
|
};
|
|
7673
|
+
var parseMirrorsField = (value) => {
|
|
7674
|
+
const fields = ownFields(value);
|
|
7675
|
+
const corrupt = () => setupError(
|
|
7676
|
+
"Server config is corrupt: mirrors must hold npm and docker, each an object or null."
|
|
7677
|
+
);
|
|
7678
|
+
if (fields === null || !fields.has("npm") || !fields.has("docker")) throw corrupt();
|
|
7679
|
+
const npm = fields.get("npm");
|
|
7680
|
+
const docker2 = fields.get("docker");
|
|
7681
|
+
let parsedNpm = null;
|
|
7682
|
+
if (npm !== null) {
|
|
7683
|
+
const maxSize = ownFields(npm)?.get("maxSize");
|
|
7684
|
+
if (typeof maxSize !== "string" || parseMirrorSize(maxSize) !== maxSize) throw corrupt();
|
|
7685
|
+
parsedNpm = { maxSize };
|
|
7686
|
+
}
|
|
7687
|
+
let parsedDocker = null;
|
|
7688
|
+
if (docker2 !== null) {
|
|
7689
|
+
const dockerFields = ownFields(docker2);
|
|
7690
|
+
if (dockerFields === null) throw corrupt();
|
|
7691
|
+
const maxSize = dockerFields.get("maxSize");
|
|
7692
|
+
if (typeof maxSize !== "string" || parseMirrorSize(maxSize) !== maxSize) throw corrupt();
|
|
7693
|
+
const user = dockerFields.get("upstreamUser");
|
|
7694
|
+
if (user !== void 0 && (typeof user !== "string" || !isDockerHubCredential(user)))
|
|
7695
|
+
throw corrupt();
|
|
7696
|
+
if (user !== void 0 && dockerFields.get("upstreamPublicOnly") !== true)
|
|
7697
|
+
throw setupError(
|
|
7698
|
+
"Server config is corrupt: the Docker mirror's login lacks upstreamPublicOnly. Run mend server setup with --docker-hub-public-only and a Public Repo Read-only token, or --no-docker-hub-login."
|
|
7699
|
+
);
|
|
7700
|
+
parsedDocker = user === void 0 ? { maxSize } : { maxSize, upstreamUser: user, upstreamPublicOnly: true };
|
|
7701
|
+
}
|
|
7702
|
+
return { npm: parsedNpm, docker: parsedDocker };
|
|
7703
|
+
};
|
|
6899
7704
|
var parseUrl = (input, label) => {
|
|
6900
7705
|
try {
|
|
6901
7706
|
return new URL(input);
|
|
@@ -6927,9 +7732,13 @@ var resolveAppUrl = (existing, options, appPort, edgeHost) => {
|
|
|
6927
7732
|
}
|
|
6928
7733
|
return origin;
|
|
6929
7734
|
}
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
7735
|
+
if (existing?.edgeHost !== void 0) {
|
|
7736
|
+
return parseHttpOrigin(options.url ?? `http://localhost:${appPort}`, "--url");
|
|
7737
|
+
}
|
|
7738
|
+
const saved = existing === null ? null : new URL(existing.appUrl);
|
|
7739
|
+
const savedPointsAtOldPort = saved !== null && existing !== null && options.appPort !== void 0 && saved.protocol === "http:" && saved.port !== "" && Number(saved.port) === existing.appPort;
|
|
7740
|
+
if (saved !== null && savedPointsAtOldPort) saved.port = String(appPort);
|
|
7741
|
+
const fallback = saved === null ? `http://localhost:${appPort}` : saved.origin;
|
|
6933
7742
|
return parseHttpOrigin(options.url ?? fallback, "--url");
|
|
6934
7743
|
};
|
|
6935
7744
|
var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
|
|
@@ -6944,7 +7753,7 @@ var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
|
|
|
6944
7753
|
);
|
|
6945
7754
|
}
|
|
6946
7755
|
};
|
|
6947
|
-
var checkPosture = (bind, ports, edgeHost, exposure) => {
|
|
7756
|
+
var checkPosture = (bind, sshBind, ports, edgeHost, exposure, declared) => {
|
|
6948
7757
|
if (edgeHost !== void 0 && [ports.appPort, ports.sshPort].some((p) => p === 80 || p === 443)) {
|
|
6949
7758
|
throw setupError(
|
|
6950
7759
|
"With an edge, --port and --ssh-port must not be 80 or 443: the edge publishes both on every interface."
|
|
@@ -6965,6 +7774,16 @@ var checkPosture = (bind, ports, edgeHost, exposure) => {
|
|
|
6965
7774
|
"--exposure loopback contradicts a non-loopback --bind: the port is published beyond this machine."
|
|
6966
7775
|
);
|
|
6967
7776
|
}
|
|
7777
|
+
if (exposure === "loopback" && !isLoopbackBind(sshBind)) {
|
|
7778
|
+
throw setupError(
|
|
7779
|
+
"--exposure loopback contradicts a non-loopback --ssh-bind: the SSH port is published beyond this machine."
|
|
7780
|
+
);
|
|
7781
|
+
}
|
|
7782
|
+
if (exposure === "public" && !isLoopbackBind(sshBind) && !declared.includes("workspace-ssh")) {
|
|
7783
|
+
throw setupError(
|
|
7784
|
+
`--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.`
|
|
7785
|
+
);
|
|
7786
|
+
}
|
|
6968
7787
|
if (exposure === "public" && edgeHost === void 0) {
|
|
6969
7788
|
throw setupError(
|
|
6970
7789
|
"--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 +7802,18 @@ var validateExposure = (existing, options) => {
|
|
|
6983
7802
|
"--bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
|
|
6984
7803
|
);
|
|
6985
7804
|
}
|
|
7805
|
+
const requestedSshBind = options.sshBind ?? existing?.sshBind;
|
|
7806
|
+
if (requestedSshBind !== void 0 && net3.isIP(requestedSshBind) === 0) {
|
|
7807
|
+
throw setupError(
|
|
7808
|
+
"--ssh-bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
|
|
7809
|
+
);
|
|
7810
|
+
}
|
|
7811
|
+
const sshBind = requestedSshBind === bind ? void 0 : requestedSshBind;
|
|
6986
7812
|
const edgeHost = options.noEdge ? void 0 : options.edge ?? existing?.edgeHost;
|
|
6987
7813
|
const exposure = options.exposure ?? existing?.exposure;
|
|
6988
7814
|
const tenancy = options.tenancy ?? existing?.tenancy;
|
|
6989
|
-
|
|
7815
|
+
const declared = options.declared ?? existing?.declared ?? [];
|
|
7816
|
+
checkPosture(bind, sshBind ?? bind, { appPort, sshPort }, edgeHost, exposure, declared);
|
|
6990
7817
|
const appUrl = resolveAppUrl(existing, options, appPort, edgeHost);
|
|
6991
7818
|
if (edgeHost === void 0)
|
|
6992
7819
|
checkExposurePair(bind, appUrl, options.url === void 0 && existing === null);
|
|
@@ -6997,13 +7824,15 @@ var validateExposure = (existing, options) => {
|
|
|
6997
7824
|
];
|
|
6998
7825
|
return {
|
|
6999
7826
|
bind,
|
|
7827
|
+
...sshBind === void 0 ? {} : { sshBind },
|
|
7000
7828
|
appUrl,
|
|
7001
7829
|
allowedOrigins,
|
|
7002
7830
|
appPort,
|
|
7003
7831
|
sshPort,
|
|
7004
7832
|
...edgeHost === void 0 ? {} : { edgeHost },
|
|
7005
7833
|
...exposure === void 0 ? {} : { exposure },
|
|
7006
|
-
...tenancy === void 0 ? {} : { tenancy }
|
|
7834
|
+
...tenancy === void 0 ? {} : { tenancy },
|
|
7835
|
+
...declared.length === 0 ? {} : { declared }
|
|
7007
7836
|
};
|
|
7008
7837
|
};
|
|
7009
7838
|
var parseServerConfig = (raw) => {
|
|
@@ -7042,6 +7871,13 @@ var parseServerConfig = (raw) => {
|
|
|
7042
7871
|
if (tenancy !== void 0 && !isTenancy(tenancy)) {
|
|
7043
7872
|
throw setupError(`Server config is corrupt: tenancy must be one of ${TENANCIES.join(", ")}.`);
|
|
7044
7873
|
}
|
|
7874
|
+
const declaredField = fields.get("declared");
|
|
7875
|
+
if (declaredField !== void 0 && (!Array.isArray(declaredField) || !declaredField.every((item) => typeof item === "string" && isDeclarableItem(item)))) {
|
|
7876
|
+
throw setupError(
|
|
7877
|
+
`Server config is corrupt: declared must be an array of ${DECLARABLE_ITEMS.join(", ")}.`
|
|
7878
|
+
);
|
|
7879
|
+
}
|
|
7880
|
+
const declared = Array.isArray(declaredField) ? declaredField.filter((item) => isDeclarableItem(String(item))) : [];
|
|
7045
7881
|
const config = {
|
|
7046
7882
|
schemaVersion: requiredInteger(fields, "schemaVersion"),
|
|
7047
7883
|
assetContract: requiredString(fields, "assetContract"),
|
|
@@ -7051,6 +7887,7 @@ var parseServerConfig = (raw) => {
|
|
|
7051
7887
|
dockerSocket: parseDockerSocketPath(requiredString(fields, "dockerSocket")),
|
|
7052
7888
|
dockerSocketSource,
|
|
7053
7889
|
bind: requiredString(fields, "bind"),
|
|
7890
|
+
...fields.has("sshBind") ? { sshBind: requiredString(fields, "sshBind") } : {},
|
|
7054
7891
|
appUrl: parseHttpOrigin(requiredString(fields, "appUrl"), "Server config appUrl"),
|
|
7055
7892
|
allowedOrigins: origins.map(
|
|
7056
7893
|
(origin) => parseHttpOrigin(origin, "Server config allowedOrigins")
|
|
@@ -7061,7 +7898,9 @@ var parseServerConfig = (raw) => {
|
|
|
7061
7898
|
...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {},
|
|
7062
7899
|
...edgeHost === void 0 ? {} : { edgeHost },
|
|
7063
7900
|
...exposure === void 0 ? {} : { exposure },
|
|
7064
|
-
...tenancy === void 0 ? {} : { tenancy }
|
|
7901
|
+
...tenancy === void 0 ? {} : { tenancy },
|
|
7902
|
+
...declared.length === 0 ? {} : { declared },
|
|
7903
|
+
...fields.has("mirrors") ? { mirrors: parseMirrorsField(fields.get("mirrors")) } : {}
|
|
7065
7904
|
};
|
|
7066
7905
|
if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
|
|
7067
7906
|
throw setupError(
|
|
@@ -7076,6 +7915,7 @@ var parseServerConfig = (raw) => {
|
|
|
7076
7915
|
context: void 0,
|
|
7077
7916
|
version: void 0,
|
|
7078
7917
|
bind: config.bind,
|
|
7918
|
+
sshBind: config.sshBind,
|
|
7079
7919
|
url: config.appUrl,
|
|
7080
7920
|
origins: config.allowedOrigins,
|
|
7081
7921
|
appPort: config.appPort,
|
|
@@ -7086,7 +7926,15 @@ var parseServerConfig = (raw) => {
|
|
|
7086
7926
|
edge: config.edgeHost,
|
|
7087
7927
|
noEdge: false,
|
|
7088
7928
|
exposure: config.exposure,
|
|
7089
|
-
tenancy: config.tenancy
|
|
7929
|
+
tenancy: config.tenancy,
|
|
7930
|
+
declared: config.declared,
|
|
7931
|
+
npmMirror: void 0,
|
|
7932
|
+
npmMirrorMaxSize: void 0,
|
|
7933
|
+
dockerMirror: void 0,
|
|
7934
|
+
dockerMirrorMaxSize: void 0,
|
|
7935
|
+
dockerHubUsername: void 0,
|
|
7936
|
+
dockerHubTokenStdin: false,
|
|
7937
|
+
noDockerHubLogin: false
|
|
7090
7938
|
});
|
|
7091
7939
|
return config;
|
|
7092
7940
|
};
|
|
@@ -7117,8 +7965,12 @@ var parseSecrets = (raw) => {
|
|
|
7117
7965
|
betterAuthSecret: envValue(fields, "BETTER_AUTH_SECRET"),
|
|
7118
7966
|
sealantCredentialsKey: envValue(fields, "SEALANT_CREDENTIALS_KEY"),
|
|
7119
7967
|
sealantServiceKey: envValue(fields, "SEALANT_SERVICE_KEY"),
|
|
7120
|
-
workspaceSshGatewayToken: envValue(fields, "WORKSPACE_SSH_GATEWAY_TOKEN")
|
|
7968
|
+
workspaceSshGatewayToken: envValue(fields, "WORKSPACE_SSH_GATEWAY_TOKEN"),
|
|
7969
|
+
...fields.has("MEND_DOCKER_HUB_TOKEN") ? { dockerHubToken: envValue(fields, "MEND_DOCKER_HUB_TOKEN") } : {}
|
|
7121
7970
|
};
|
|
7971
|
+
if (secrets.dockerHubToken !== void 0 && !isDockerHubCredential(secrets.dockerHubToken)) {
|
|
7972
|
+
throw setupError("Server secrets are corrupt: MEND_DOCKER_HUB_TOKEN has an invalid value.");
|
|
7973
|
+
}
|
|
7122
7974
|
if (!/^slt_svc_[0-9a-f]{64}$/.test(secrets.sealantServiceKey)) {
|
|
7123
7975
|
throw setupError("Server secrets are corrupt: SEALANT_SERVICE_KEY has an invalid value.");
|
|
7124
7976
|
}
|
|
@@ -7423,9 +8275,10 @@ var renderGarage = (secrets, config) => {
|
|
|
7423
8275
|
];
|
|
7424
8276
|
};
|
|
7425
8277
|
var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
8278
|
+
var composeHost = (address) => net3.isIP(address) === 6 ? `[${address}]` : address;
|
|
7426
8279
|
var renderSecrets = (secrets, config) => {
|
|
7427
8280
|
const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
|
|
7428
|
-
const composeBind =
|
|
8281
|
+
const composeBind = composeHost(config.bind);
|
|
7429
8282
|
return [
|
|
7430
8283
|
`MEND_VERSION=${config.serverVersion}`,
|
|
7431
8284
|
"MEND_IMAGE_REPOSITORY=ghcr.io/sealant-sh/mend",
|
|
@@ -7433,6 +8286,7 @@ var renderSecrets = (secrets, config) => {
|
|
|
7433
8286
|
`APP_URL=${config.appUrl}`,
|
|
7434
8287
|
`MEND_ALLOWED_ORIGINS=${JSON.stringify(config.allowedOrigins)}`,
|
|
7435
8288
|
`MEND_BIND_HOST=${composeBind}`,
|
|
8289
|
+
...config.sshBind === void 0 ? [] : [`MEND_SSH_BIND_HOST=${composeHost(config.sshBind)}`],
|
|
7436
8290
|
`MEND_PORT=${config.appPort}`,
|
|
7437
8291
|
`MEND_SSH_PORT=${config.sshPort}`,
|
|
7438
8292
|
...config.registryPort === void 0 ? [] : [`MEND_REGISTRY_PORT=${config.registryPort}`],
|
|
@@ -7443,6 +8297,9 @@ var renderSecrets = (secrets, config) => {
|
|
|
7443
8297
|
"MEND_STORE_VOLUME_NAME=mend-store",
|
|
7444
8298
|
"MEND_CONTROL_VOLUME_NAME=mend-control",
|
|
7445
8299
|
...renderGarage(secrets, config),
|
|
8300
|
+
// The mirrors' size cap and Docker Hub login (server-mirrors.ts); the token reaches only the
|
|
8301
|
+
// Docker mirror's container.
|
|
8302
|
+
...mirrorsEnvLines(config.mirrors, secrets.dockerHubToken),
|
|
7446
8303
|
`DOCKER_SOCKET_PATH=${config.dockerSocket}`,
|
|
7447
8304
|
""
|
|
7448
8305
|
].join("\n");
|
|
@@ -7493,6 +8350,47 @@ var validateOverlays = (config, files) => {
|
|
|
7493
8350
|
}
|
|
7494
8351
|
return edgeImageOf(files.edge);
|
|
7495
8352
|
};
|
|
8353
|
+
var validateMirrors = (config, secrets, files) => {
|
|
8354
|
+
const mirrors = config.mirrors;
|
|
8355
|
+
if (runsMirrors(mirrors) !== (files.mirrors !== void 0)) {
|
|
8356
|
+
throw setupError(
|
|
8357
|
+
`Server generation is corrupt: ${MIRRORS_COMPOSE_FILE} does not match the persisted server config.`
|
|
8358
|
+
);
|
|
8359
|
+
}
|
|
8360
|
+
if ((mirrors !== void 0 && mirrors.npm !== null) !== (files.npmMirrorConf !== void 0)) {
|
|
8361
|
+
throw setupError(
|
|
8362
|
+
`Server generation is corrupt: ${NPM_MIRROR_CONF_NAME} does not match the persisted server config.`
|
|
8363
|
+
);
|
|
8364
|
+
}
|
|
8365
|
+
if (dockerMirrorLogin(mirrors?.docker ?? null) === void 0 !== (secrets.dockerHubToken === void 0)) {
|
|
8366
|
+
throw setupError(
|
|
8367
|
+
"Server secrets are corrupt: a Docker Hub login needs both MEND_DOCKER_HUB_USERNAME and MEND_DOCKER_HUB_TOKEN."
|
|
8368
|
+
);
|
|
8369
|
+
}
|
|
8370
|
+
if (files.mirrors === void 0) return [];
|
|
8371
|
+
for (const service of mirrorServices(mirrors)) {
|
|
8372
|
+
if (!files.mirrors.includes(`
|
|
8373
|
+
${service}:
|
|
8374
|
+
`))
|
|
8375
|
+
throw setupError(
|
|
8376
|
+
`Server generation is corrupt: ${MIRRORS_COMPOSE_FILE} does not run the ${service} its config declares.`
|
|
8377
|
+
);
|
|
8378
|
+
}
|
|
8379
|
+
if ((mirrors !== void 0 && mirrors.docker !== null) !== (files.dockerMirrorGuard !== void 0)) {
|
|
8380
|
+
throw setupError(
|
|
8381
|
+
`Server generation is corrupt: ${DOCKER_MIRROR_GUARD_NAME} does not match the persisted server config.`
|
|
8382
|
+
);
|
|
8383
|
+
}
|
|
8384
|
+
if (files.dockerMirrorGuard !== void 0 && !files.dockerMirrorGuard.includes("registry serve /etc/distribution/config.yml"))
|
|
8385
|
+
throw setupError(
|
|
8386
|
+
`Server generation is corrupt: ${DOCKER_MIRROR_GUARD_NAME} is not the Docker mirror's guard.`
|
|
8387
|
+
);
|
|
8388
|
+
if (files.npmMirrorConf !== void 0 && !files.npmMirrorConf.includes("proxy_cache npm;"))
|
|
8389
|
+
throw setupError(
|
|
8390
|
+
`Server generation is corrupt: ${NPM_MIRROR_CONF_NAME} is not the npm mirror's configuration.`
|
|
8391
|
+
);
|
|
8392
|
+
return mirrorImagesOf(files.mirrors);
|
|
8393
|
+
};
|
|
7496
8394
|
var readServerInstallation = (store) => {
|
|
7497
8395
|
try {
|
|
7498
8396
|
const generation = storeValue(store.readActive());
|
|
@@ -7507,12 +8405,14 @@ var readServerInstallation = (store) => {
|
|
|
7507
8405
|
validateComposeAsset(generation.files.compose);
|
|
7508
8406
|
validatePostgresAsset(generation.files.postgresInit);
|
|
7509
8407
|
const edgeImage = validateOverlays(config, generation.files);
|
|
8408
|
+
const mirrorImages = validateMirrors(config, secrets, generation.files);
|
|
7510
8409
|
return {
|
|
7511
8410
|
_tag: "ok",
|
|
7512
8411
|
value: {
|
|
7513
8412
|
config,
|
|
7514
8413
|
directory: generation.directory,
|
|
7515
|
-
...edgeImage === void 0 ? {} : { edgeImage }
|
|
8414
|
+
...edgeImage === void 0 ? {} : { edgeImage },
|
|
8415
|
+
...mirrorImages.length === 0 ? {} : { mirrorImages }
|
|
7516
8416
|
}
|
|
7517
8417
|
};
|
|
7518
8418
|
} catch (cause) {
|
|
@@ -7543,7 +8443,10 @@ var readServerInstallationFacts = async (configDir) => {
|
|
|
7543
8443
|
overlays: [
|
|
7544
8444
|
...generation.files.edge === void 0 ? [] : [{ name: "compose.edge.yaml", content: generation.files.edge }],
|
|
7545
8445
|
...generation.files.caddyfile === void 0 ? [] : [{ name: "Caddyfile", content: generation.files.caddyfile }],
|
|
7546
|
-
...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }]
|
|
8446
|
+
...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }],
|
|
8447
|
+
...generation.files.mirrors === void 0 ? [] : [{ name: MIRRORS_COMPOSE_FILE, content: generation.files.mirrors }],
|
|
8448
|
+
...generation.files.npmMirrorConf === void 0 ? [] : [{ name: NPM_MIRROR_CONF_NAME, content: generation.files.npmMirrorConf }],
|
|
8449
|
+
...generation.files.dockerMirrorGuard === void 0 ? [] : [{ name: DOCKER_MIRROR_GUARD_NAME, content: generation.files.dockerMirrorGuard }]
|
|
7547
8450
|
],
|
|
7548
8451
|
envKeys: envKeyNames(generation.files.env)
|
|
7549
8452
|
};
|
|
@@ -7555,6 +8458,7 @@ var readServerInstallationFacts = async (configDir) => {
|
|
|
7555
8458
|
};
|
|
7556
8459
|
var generationFiles = (config, secrets, assets, identity = renderIdentity(secrets)) => {
|
|
7557
8460
|
const posture = renderPostureOverlay(config);
|
|
8461
|
+
const mirrors = renderMirrorsOverlay(config.mirrors);
|
|
7558
8462
|
return {
|
|
7559
8463
|
identity,
|
|
7560
8464
|
config: `${JSON.stringify(config, null, 2)}
|
|
@@ -7562,7 +8466,10 @@ var generationFiles = (config, secrets, assets, identity = renderIdentity(secret
|
|
|
7562
8466
|
env: renderSecrets(secrets, config),
|
|
7563
8467
|
...assets,
|
|
7564
8468
|
...posture === void 0 ? {} : { posture },
|
|
7565
|
-
...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE }
|
|
8469
|
+
...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE },
|
|
8470
|
+
...mirrors === void 0 ? {} : { mirrors },
|
|
8471
|
+
...config.mirrors === void 0 || config.mirrors.npm === null ? {} : { npmMirrorConf: NPM_MIRROR_CONF },
|
|
8472
|
+
...config.mirrors === void 0 || config.mirrors.docker === null ? {} : { dockerMirrorGuard: DOCKER_MIRROR_GUARD }
|
|
7566
8473
|
};
|
|
7567
8474
|
};
|
|
7568
8475
|
var persistSetup = (store, config, secrets, assets) => storeValue(store.prepare(generationFiles(config, secrets, assets)));
|
|
@@ -7632,6 +8539,13 @@ var resolveDockerSocket = (runtime, options, existing, selectedContext) => {
|
|
|
7632
8539
|
dockerSocketSource: override === void 0 ? "detected" : "override"
|
|
7633
8540
|
};
|
|
7634
8541
|
};
|
|
8542
|
+
var checkSshBindAsset = (config, compose) => {
|
|
8543
|
+
if (config.sshBind !== void 0 && !compose.includes("MEND_SSH_BIND_HOST")) {
|
|
8544
|
+
throw setupError(
|
|
8545
|
+
`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.`
|
|
8546
|
+
);
|
|
8547
|
+
}
|
|
8548
|
+
};
|
|
7635
8549
|
var resolveAssets = async (runtime, serverVersion, existing, store, options) => {
|
|
7636
8550
|
if (options.assetsDir !== void 0) {
|
|
7637
8551
|
try {
|
|
@@ -7724,6 +8638,17 @@ var checkLocalImages = async (runtime, installation, policy = "local") => {
|
|
|
7724
8638
|
if (edge.status !== 0)
|
|
7725
8639
|
throw commandFailure(`Preload ${installation.edgeImage} before continuing`, edge);
|
|
7726
8640
|
}
|
|
8641
|
+
for (const mirrorImage of installation.mirrorImages ?? []) {
|
|
8642
|
+
const mirror = await inspectImage(
|
|
8643
|
+
runtime,
|
|
8644
|
+
config.dockerContext,
|
|
8645
|
+
mirrorImage,
|
|
8646
|
+
"{{.Id}}",
|
|
8647
|
+
policy
|
|
8648
|
+
);
|
|
8649
|
+
if (mirror.status !== 0)
|
|
8650
|
+
throw commandFailure(`Preload ${mirrorImage} before continuing`, mirror);
|
|
8651
|
+
}
|
|
7727
8652
|
};
|
|
7728
8653
|
var checkComposeImages = async (runtime, installation) => {
|
|
7729
8654
|
const output = await runtime.run(
|
|
@@ -7736,11 +8661,12 @@ var checkComposeImages = async (runtime, installation) => {
|
|
|
7736
8661
|
`ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`,
|
|
7737
8662
|
"postgres:17-alpine",
|
|
7738
8663
|
...installation.config.bucket === "garage" ? [GARAGE_IMAGE] : [],
|
|
7739
|
-
...installation.edgeImage === void 0 ? [] : [installation.edgeImage]
|
|
8664
|
+
...installation.edgeImage === void 0 ? [] : [installation.edgeImage],
|
|
8665
|
+
...installation.mirrorImages ?? []
|
|
7740
8666
|
].toSorted();
|
|
7741
8667
|
if (images.join("\n") !== expected.join("\n")) {
|
|
7742
8668
|
throw setupError(
|
|
7743
|
-
`Compose must use only the canonical pinned Mend image, official postgres:17-alpine${installation.config.bucket === "garage" ? ` and ${GARAGE_IMAGE}` : ""}${installation.edgeImage === void 0 ? "" : ` and the edge's ${installation.edgeImage}`}.`
|
|
8669
|
+
`Compose must use only the canonical pinned Mend image, official postgres:17-alpine${installation.config.bucket === "garage" ? ` and ${GARAGE_IMAGE}` : ""}${installation.edgeImage === void 0 ? "" : ` and the edge's ${installation.edgeImage}`}${installation.mirrorImages === void 0 ? "" : ` and the mirrors' ${installation.mirrorImages.join(", ")}`}.`
|
|
7744
8670
|
);
|
|
7745
8671
|
}
|
|
7746
8672
|
};
|
|
@@ -7787,8 +8713,16 @@ var initGarage = async (runtime, installation, secrets) => {
|
|
|
7787
8713
|
var startingNotice = (runtime, version) => runtime.writeLine(
|
|
7788
8714
|
`Starting Mend ${version} containers; Docker waits up to ${serverProcessDeadlines.composeWaitSeconds}s for them to report healthy`
|
|
7789
8715
|
);
|
|
7790
|
-
var
|
|
7791
|
-
|
|
8716
|
+
var strayServices = (config) => [
|
|
8717
|
+
...config.edgeHost === void 0 ? [{ service: "edge", label: "edge", holds: "It may still hold 80 and 443." }] : [],
|
|
8718
|
+
...config.mirrors?.npm === null ? [{ service: "npm-mirror", label: "npm mirror", holds: "It keeps running beside Mend." }] : [],
|
|
8719
|
+
...config.mirrors?.docker === null ? [{ service: "docker-mirror", label: "Docker mirror", holds: "It keeps running beside Mend." }] : []
|
|
8720
|
+
];
|
|
8721
|
+
var removeStrayServices = async (runtime, installation) => {
|
|
8722
|
+
for (const stray of strayServices(installation.config))
|
|
8723
|
+
await removeStrayService(runtime, installation, stray);
|
|
8724
|
+
};
|
|
8725
|
+
var removeStrayService = async (runtime, installation, stray) => {
|
|
7792
8726
|
const context = installation.config.dockerContext;
|
|
7793
8727
|
const listed = await runtime.run("docker", [
|
|
7794
8728
|
"--context",
|
|
@@ -7799,13 +8733,13 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7799
8733
|
"--filter",
|
|
7800
8734
|
"label=com.docker.compose.project=mend",
|
|
7801
8735
|
"--filter",
|
|
7802
|
-
|
|
8736
|
+
`label=com.docker.compose.service=${stray.service}`,
|
|
7803
8737
|
"--format",
|
|
7804
8738
|
'{{.Names}} {{.Label "com.docker.compose.project.working_dir"}}'
|
|
7805
8739
|
]);
|
|
7806
8740
|
if (listed.status !== 0 || listed.error !== void 0) {
|
|
7807
8741
|
runtime.writeLine(
|
|
7808
|
-
`Could not list this project's containers: ${outputDetail(listed)}. An
|
|
8742
|
+
`Could not list this project's containers: ${outputDetail(listed)}. An ${stray.label} container left by an earlier generation, if any, stays until the next start. mend server status shows it.`
|
|
7809
8743
|
);
|
|
7810
8744
|
return;
|
|
7811
8745
|
}
|
|
@@ -7821,9 +8755,10 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7821
8755
|
const [name = "", workingDir = ""] = line.split(" ");
|
|
7822
8756
|
return { name, workingDir };
|
|
7823
8757
|
}).filter((row) => row.name !== "");
|
|
8758
|
+
const label = stray.label.charAt(0).toUpperCase() + stray.label.slice(1);
|
|
7824
8759
|
for (const row of rows.filter((candidate) => !own(candidate.workingDir))) {
|
|
7825
8760
|
runtime.writeLine(
|
|
7826
|
-
|
|
8761
|
+
`${label} container ${row.name} was started from ${row.workingDir || "an unknown directory"}, not from this installation's generations. It stays.`
|
|
7827
8762
|
);
|
|
7828
8763
|
}
|
|
7829
8764
|
const names = rows.filter((row) => own(row.workingDir)).map((row) => row.name);
|
|
@@ -7838,16 +8773,16 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7838
8773
|
]);
|
|
7839
8774
|
if (removed.status !== 0 || removed.error !== void 0) {
|
|
7840
8775
|
runtime.writeLine(
|
|
7841
|
-
`Could not remove the
|
|
8776
|
+
`Could not remove the ${stray.label} container ${names.join(", ")}: ${outputDetail(removed)}. ${stray.holds} To remove it: docker --context ${context} container rm --force ${names.join(" ")}`
|
|
7842
8777
|
);
|
|
7843
8778
|
return;
|
|
7844
8779
|
}
|
|
7845
8780
|
runtime.writeLine(
|
|
7846
|
-
`Removed the
|
|
8781
|
+
`Removed the ${stray.label} container ${names.join(", ")}, which this generation does not run.`
|
|
7847
8782
|
);
|
|
7848
8783
|
};
|
|
7849
8784
|
var startCompose = async (runtime, installation) => {
|
|
7850
|
-
await
|
|
8785
|
+
await removeStrayServices(runtime, installation);
|
|
7851
8786
|
startingNotice(runtime, installation.config.serverVersion);
|
|
7852
8787
|
const compose = await runtime.run(
|
|
7853
8788
|
"docker",
|
|
@@ -7865,7 +8800,70 @@ var startCompose = async (runtime, installation) => {
|
|
|
7865
8800
|
);
|
|
7866
8801
|
if (compose.status !== 0) throw commandFailure("Mend containers did not start", compose);
|
|
7867
8802
|
};
|
|
7868
|
-
var
|
|
8803
|
+
var SSH_PROBE_BOUND_MS = 1e4;
|
|
8804
|
+
var sshPublicationLine = async (runtime, config) => {
|
|
8805
|
+
if (config.sshBind === void 0 || isLoopbackBind(config.sshBind)) return null;
|
|
8806
|
+
const published = publishedAddress(config.sshBind, config.sshPort);
|
|
8807
|
+
if (runtime.probeSsh === void 0) return null;
|
|
8808
|
+
const boundMs = runtime.sshProbeBoundMs ?? SSH_PROBE_BOUND_MS;
|
|
8809
|
+
let bound;
|
|
8810
|
+
const probes = await Promise.race([
|
|
8811
|
+
runtime.probeSsh(config.sshBind, config.sshPort),
|
|
8812
|
+
new Promise((resolve8) => {
|
|
8813
|
+
bound = setTimeout(() => resolve8(null), boundMs);
|
|
8814
|
+
})
|
|
8815
|
+
]).finally(() => clearTimeout(bound));
|
|
8816
|
+
if (probes === null) {
|
|
8817
|
+
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.`;
|
|
8818
|
+
}
|
|
8819
|
+
const observed = probes.length === 0 ? "this machine has no address to try it at" : probes.map(
|
|
8820
|
+
(probe) => probe.banner === null ? `${publishedAddress(probe.address, config.sshPort)} did not answer` : `${publishedAddress(probe.address, config.sshPort)} answers (${probe.banner})`
|
|
8821
|
+
).join(", ");
|
|
8822
|
+
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.`;
|
|
8823
|
+
};
|
|
8824
|
+
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}.`}`;
|
|
8825
|
+
var resolveDockerHubToken = async (runtime, options, mirrors, store) => {
|
|
8826
|
+
if (dockerMirrorLogin(mirrors.docker) === void 0) return void 0;
|
|
8827
|
+
if (options.dockerHubTokenStdin) {
|
|
8828
|
+
if (runtime.readStdin === void 0)
|
|
8829
|
+
throw setupError("--docker-hub-token-stdin needs standard input, and none is available.");
|
|
8830
|
+
const token = (await runtime.readStdin()).trim();
|
|
8831
|
+
if (!isDockerHubCredential(token))
|
|
8832
|
+
throw setupError(
|
|
8833
|
+
"--docker-hub-token-stdin read no Docker Hub access token: pipe the token alone, such as a personal access token (dckr_pat_\u2026)."
|
|
8834
|
+
);
|
|
8835
|
+
return token;
|
|
8836
|
+
}
|
|
8837
|
+
const active = storeValue(store.readActive());
|
|
8838
|
+
return active === null ? void 0 : parseSecrets(active.files.env).dockerHubToken;
|
|
8839
|
+
};
|
|
8840
|
+
var mirrorsChangedLines = (before, config) => {
|
|
8841
|
+
const after = config.mirrors;
|
|
8842
|
+
if (after === void 0) return [];
|
|
8843
|
+
const lines = [];
|
|
8844
|
+
const said = (name, was, is, volume, reach) => {
|
|
8845
|
+
if (is && !was) lines.push(`The ${name} runs on this install. ${reach}`);
|
|
8846
|
+
if (was && !is)
|
|
8847
|
+
lines.push(
|
|
8848
|
+
`The ${name} is off. Its cache stays until you remove it: docker --context ${config.dockerContext} volume rm ${volume}`
|
|
8849
|
+
);
|
|
8850
|
+
};
|
|
8851
|
+
said(
|
|
8852
|
+
"npm mirror",
|
|
8853
|
+
before !== void 0 && before.npm !== null,
|
|
8854
|
+
after.npm !== null,
|
|
8855
|
+
"mend_mend-npm-mirror",
|
|
8856
|
+
`New sessions install npm packages through it, capped at ${after.npm?.maxSize ?? ""}.`
|
|
8857
|
+
);
|
|
8858
|
+
said(
|
|
8859
|
+
"Docker mirror",
|
|
8860
|
+
before !== void 0 && before.docker !== null,
|
|
8861
|
+
after.docker !== null,
|
|
8862
|
+
"mend_mend-docker-mirror",
|
|
8863
|
+
`New sessions' Docker daemons pull Docker Hub images through it (${DOCKER_MIRROR_CONTAINER}).`
|
|
8864
|
+
);
|
|
8865
|
+
return lines;
|
|
8866
|
+
};
|
|
7869
8867
|
var setupServer = async (args, runtime, store) => {
|
|
7870
8868
|
if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
|
|
7871
8869
|
throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
|
|
@@ -7891,6 +8889,7 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7891
8889
|
const operatingSystem = await checkDocker(runtime, selectedContext.name);
|
|
7892
8890
|
runtime.writeLine(`Using Docker context "${selectedContext.name}" (${selectedContext.endpoint})`);
|
|
7893
8891
|
const serverVersion = await resolveServerVersion(runtime, options, existing?.config ?? null);
|
|
8892
|
+
const mirrors = resolveMirrors(existing?.config.mirrors, options);
|
|
7894
8893
|
const configWithoutBucket = {
|
|
7895
8894
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
7896
8895
|
assetContract: ASSET_CONTRACT,
|
|
@@ -7901,9 +8900,12 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7901
8900
|
...selectedContext,
|
|
7902
8901
|
operatingSystem
|
|
7903
8902
|
}),
|
|
7904
|
-
...validateExposure(existing?.config ?? null, options)
|
|
8903
|
+
...validateExposure(existing?.config ?? null, options),
|
|
8904
|
+
mirrors
|
|
7905
8905
|
};
|
|
8906
|
+
const dockerHubToken = await resolveDockerHubToken(runtime, options, mirrors, store);
|
|
7906
8907
|
const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
|
|
8908
|
+
checkSshBindAsset(configWithoutBucket, assets.compose);
|
|
7907
8909
|
const bucket = composeBucket(assets.compose);
|
|
7908
8910
|
if (bucket !== void 0 && runtime.dockerDaemonFacts !== void 0) {
|
|
7909
8911
|
const info = await runtime.run("docker", [
|
|
@@ -7927,12 +8929,16 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7927
8929
|
`A fresh install cannot start with the edge or as public: until the first account exists, registration is open to whoever reaches the origin first, and the server refuses MEND_EXPOSURE=public without an operator account. Run mend server setup without --edge and --exposure public, create the first account at http://localhost:${config.appPort}, then run mend server setup --edge <host> and declare the posture.`
|
|
7928
8930
|
);
|
|
7929
8931
|
}
|
|
7930
|
-
const secrets =
|
|
8932
|
+
const secrets = {
|
|
8933
|
+
...savedSecrets ?? createSecrets(runtime),
|
|
8934
|
+
...dockerHubToken === void 0 ? {} : { dockerHubToken }
|
|
8935
|
+
};
|
|
7931
8936
|
const generation = persistSetup(store, config, secrets, assets);
|
|
7932
8937
|
const installation = {
|
|
7933
8938
|
directory: generation.directory,
|
|
7934
8939
|
config,
|
|
7935
|
-
...generation.files.edge === void 0 ? {} : { edgeImage: edgeImageOf(generation.files.edge) }
|
|
8940
|
+
...generation.files.edge === void 0 ? {} : { edgeImage: edgeImageOf(generation.files.edge) },
|
|
8941
|
+
...generation.files.mirrors === void 0 ? {} : { mirrorImages: mirrorImagesOf(generation.files.mirrors) }
|
|
7936
8942
|
};
|
|
7937
8943
|
await checkComposeImages(runtime, installation);
|
|
7938
8944
|
const ownership2 = await claimServerDockerVolumes(runtime, {
|
|
@@ -7954,11 +8960,14 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7954
8960
|
runtime.writeLine(reachableLine(config));
|
|
7955
8961
|
const edgeStarted = edgeStartedLine(config);
|
|
7956
8962
|
if (edgeStarted !== null) runtime.writeLine(edgeStarted);
|
|
8963
|
+
const sshPublication = await sshPublicationLine(runtime, config);
|
|
8964
|
+
if (sshPublication !== null) runtime.writeLine(sshPublication);
|
|
7957
8965
|
if (existing?.config.edgeHost !== void 0 && config.edgeHost === void 0) {
|
|
7958
8966
|
runtime.writeLine(
|
|
7959
8967
|
`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`
|
|
7960
8968
|
);
|
|
7961
8969
|
}
|
|
8970
|
+
for (const line of mirrorsChangedLines(existing?.config.mirrors, config)) runtime.writeLine(line);
|
|
7962
8971
|
runtime.writeLine(
|
|
7963
8972
|
`Open ${config.appUrl}, create the first account, then run: mend login --url ${config.appUrl}`
|
|
7964
8973
|
);
|
|
@@ -8023,7 +9032,7 @@ var interruptionNotice = (runtime) => runtime.writeLine(
|
|
|
8023
9032
|
"Connections will be interrupted. Workspace containers and data are retained, but active work can lose connectivity and may need reconnection. Mend does not stop workspace containers."
|
|
8024
9033
|
);
|
|
8025
9034
|
var startInstallation = async (runtime, installation, secrets) => {
|
|
8026
|
-
await
|
|
9035
|
+
await removeStrayServices(runtime, installation);
|
|
8027
9036
|
startingNotice(runtime, installation.config.serverVersion);
|
|
8028
9037
|
await composeCommand(runtime, installation, [
|
|
8029
9038
|
"up",
|
|
@@ -8071,7 +9080,7 @@ var parseUpgradeOptions = (args) => {
|
|
|
8071
9080
|
);
|
|
8072
9081
|
return { ...options, fromPreview, keepBackups };
|
|
8073
9082
|
};
|
|
8074
|
-
var
|
|
9083
|
+
var formatBytes4 = (bytes) => {
|
|
8075
9084
|
if (bytes < 1024) return `${bytes} B`;
|
|
8076
9085
|
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
8077
9086
|
let value = bytes / 1024;
|
|
@@ -8114,11 +9123,11 @@ var pruneUpgradeBackups = (runtime, store, backup, keep) => {
|
|
|
8114
9123
|
const { removed, kept, held, failed, unsynced } = pruned.value;
|
|
8115
9124
|
for (const entry of removed)
|
|
8116
9125
|
runtime.writeLine(
|
|
8117
|
-
`Removed upgrade backup ${entry.directory} \xB7 ${
|
|
9126
|
+
`Removed upgrade backup ${entry.directory} \xB7 ${formatBytes4(entry.bytes)}${entry.interrupted ? " \xB7 finishing a removal a crash cut short" : entry.legacy ? " \xB7 from before 0.36, no recorded outcome" : ""}`
|
|
8118
9127
|
);
|
|
8119
9128
|
const freed = removed.reduce((total, entry) => total + entry.bytes, 0);
|
|
8120
9129
|
runtime.writeLine(
|
|
8121
|
-
`Upgrade backups \xB7 removed ${removed.length}${removed.length > 0 ? ` \xB7 ${
|
|
9130
|
+
`Upgrade backups \xB7 removed ${removed.length}${removed.length > 0 ? ` \xB7 ${formatBytes4(freed)} freed` : ""} \xB7 kept ${kept.length} (--keep-backups ${keep})`
|
|
8122
9131
|
);
|
|
8123
9132
|
for (const entry of failed)
|
|
8124
9133
|
runtime.writeLine(`Could not remove upgrade backup ${entry.directory}: ${entry.message}`);
|
|
@@ -8276,6 +9285,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8276
9285
|
const previous = storeValue(store.readActive());
|
|
8277
9286
|
if (previous === null) throw setupError("The active server generation is missing.");
|
|
8278
9287
|
const assets = await resolveAssets(runtime, version, existing, store, options);
|
|
9288
|
+
checkSshBindAsset({ ...existing.config, serverVersion: version }, assets.compose);
|
|
8279
9289
|
const {
|
|
8280
9290
|
registryPort: _legacyRegistryPort,
|
|
8281
9291
|
bucket: _previousBucket,
|
|
@@ -8286,14 +9296,23 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8286
9296
|
...carried,
|
|
8287
9297
|
assetContract: ASSET_CONTRACT,
|
|
8288
9298
|
serverVersion: version,
|
|
8289
|
-
...bucket === void 0 ? {} : { bucket }
|
|
9299
|
+
...bucket === void 0 ? {} : { bucket },
|
|
9300
|
+
// An install from before the mirrors gains both here; one that turned a mirror off keeps it off.
|
|
9301
|
+
mirrors: carried.mirrors ?? DEFAULT_MIRRORS
|
|
9302
|
+
};
|
|
9303
|
+
const previousToken = parseSecrets(previous.files.env).dockerHubToken;
|
|
9304
|
+
const secrets = {
|
|
9305
|
+
...parseSecrets(previous.files.identity),
|
|
9306
|
+
...previousToken === void 0 ? {} : { dockerHubToken: previousToken }
|
|
8290
9307
|
};
|
|
8291
|
-
const secrets = parseSecrets(previous.files.identity);
|
|
8292
9308
|
const files = generationFiles(config, secrets, assets, previous.files.identity);
|
|
8293
9309
|
const parsed = parseServerConfig(files.config);
|
|
8294
9310
|
if (renderSecrets(parseSecrets(files.env), parsed) !== files.env)
|
|
8295
9311
|
throw setupError("Invalid upgrade configuration.");
|
|
8296
|
-
const edgeImage =
|
|
9312
|
+
const edgeImage = {
|
|
9313
|
+
...files.edge === void 0 ? {} : { edgeImage: edgeImageOf(files.edge) },
|
|
9314
|
+
...files.mirrors === void 0 ? {} : { mirrorImages: mirrorImagesOf(files.mirrors) }
|
|
9315
|
+
};
|
|
8297
9316
|
await checkLocalImages(
|
|
8298
9317
|
runtime,
|
|
8299
9318
|
{ config, ...edgeImage },
|
|
@@ -8378,6 +9397,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8378
9397
|
);
|
|
8379
9398
|
}
|
|
8380
9399
|
runtime.writeLine(`Upgraded to ${version}. Retained database backup: ${backup.directory}`);
|
|
9400
|
+
for (const line of mirrorsChangedLines(existing.config.mirrors, config)) runtime.writeLine(line);
|
|
8381
9401
|
pruneUpgradeBackups(runtime, store, backup, options.keepBackups);
|
|
8382
9402
|
};
|
|
8383
9403
|
var observeEdgeCertificate = async (runtime, installation, host) => {
|
|
@@ -8481,6 +9501,75 @@ var exposureReportOf = (value) => {
|
|
|
8481
9501
|
const parsed = items.flatMap(exposureItemOf);
|
|
8482
9502
|
return parsed.length === items.length && parsed.length > 0 ? { declared, items: parsed } : null;
|
|
8483
9503
|
};
|
|
9504
|
+
var mirrorStatusLines = async (runtime, installation, runningServices) => {
|
|
9505
|
+
const mirrors = installation.config.mirrors;
|
|
9506
|
+
if (mirrors === void 0) return [];
|
|
9507
|
+
const run2 = (args) => runtime.run("docker", serverComposeArgs(composeTarget(installation), args), {
|
|
9508
|
+
timeoutMs: serverProcessDeadlines.ordinary
|
|
9509
|
+
});
|
|
9510
|
+
const failed = (output) => output.status !== 0 || output.error !== void 0 ? outputDetail(output) : null;
|
|
9511
|
+
const diskOf = async (service, directory) => {
|
|
9512
|
+
const probe = await run2(["exec", "-T", service, "sh", "-c", mirrorDiskProbe(directory)]);
|
|
9513
|
+
return failed(probe) ?? mirrorDiskOf(probe.stdout) ?? "du and df answered nothing readable";
|
|
9514
|
+
};
|
|
9515
|
+
const lines = [];
|
|
9516
|
+
const npmRunning = runningServices.includes("npm-mirror");
|
|
9517
|
+
if (mirrors.npm === null || !npmRunning) {
|
|
9518
|
+
lines.push(observedNpmMirrorLine(mirrors, { running: false, disk: "", traffic: "" }));
|
|
9519
|
+
} else {
|
|
9520
|
+
const log = await run2([
|
|
9521
|
+
"logs",
|
|
9522
|
+
"--no-color",
|
|
9523
|
+
"--no-log-prefix",
|
|
9524
|
+
"--since",
|
|
9525
|
+
"24h",
|
|
9526
|
+
"npm-mirror"
|
|
9527
|
+
]);
|
|
9528
|
+
lines.push(
|
|
9529
|
+
observedNpmMirrorLine(mirrors, {
|
|
9530
|
+
running: true,
|
|
9531
|
+
disk: await diskOf("npm-mirror", "/var/cache/npm-mirror"),
|
|
9532
|
+
traffic: failed(log) ?? npmMirrorTraffic(log.stdout)
|
|
9533
|
+
})
|
|
9534
|
+
);
|
|
9535
|
+
}
|
|
9536
|
+
const dockerRunning = runningServices.includes("docker-mirror");
|
|
9537
|
+
if (mirrors.docker === null || !dockerRunning) {
|
|
9538
|
+
lines.push(
|
|
9539
|
+
observedDockerMirrorLine(mirrors, { running: false, disk: "", traffic: "", startedAt: null })
|
|
9540
|
+
);
|
|
9541
|
+
} else {
|
|
9542
|
+
const metrics = await run2([
|
|
9543
|
+
"exec",
|
|
9544
|
+
"-T",
|
|
9545
|
+
"docker-mirror",
|
|
9546
|
+
"wget",
|
|
9547
|
+
"-q",
|
|
9548
|
+
"-O",
|
|
9549
|
+
"-",
|
|
9550
|
+
"http://127.0.0.1:5001/metrics"
|
|
9551
|
+
]);
|
|
9552
|
+
const started = await runtime.run("docker", [
|
|
9553
|
+
"--context",
|
|
9554
|
+
installation.config.dockerContext,
|
|
9555
|
+
"container",
|
|
9556
|
+
"inspect",
|
|
9557
|
+
"--format",
|
|
9558
|
+
"{{.State.StartedAt}}",
|
|
9559
|
+
DOCKER_MIRROR_CONTAINER
|
|
9560
|
+
]);
|
|
9561
|
+
const startedAt = failed(started) === null ? started.stdout.trim().replace(/\.\d+Z$/, "Z") : "";
|
|
9562
|
+
lines.push(
|
|
9563
|
+
observedDockerMirrorLine(mirrors, {
|
|
9564
|
+
running: true,
|
|
9565
|
+
disk: await diskOf("docker-mirror", "/var/lib/registry"),
|
|
9566
|
+
traffic: failed(metrics) ?? dockerMirrorTraffic(metrics.stdout) ?? "the registry reported no proxy counters",
|
|
9567
|
+
startedAt: startedAt === "" ? null : startedAt
|
|
9568
|
+
})
|
|
9569
|
+
);
|
|
9570
|
+
}
|
|
9571
|
+
return lines;
|
|
9572
|
+
};
|
|
8484
9573
|
var serverStatus = async (runtime, installation) => {
|
|
8485
9574
|
const { config } = installation;
|
|
8486
9575
|
runtime.writeLine(`Pinned Mend ${config.serverVersion} at ${config.appUrl}`);
|
|
@@ -8504,6 +9593,8 @@ var serverStatus = async (runtime, installation) => {
|
|
|
8504
9593
|
})
|
|
8505
9594
|
);
|
|
8506
9595
|
}
|
|
9596
|
+
for (const line of await mirrorStatusLines(runtime, installation, runningServices))
|
|
9597
|
+
runtime.writeLine(line);
|
|
8507
9598
|
if (!runningServices.includes("mend")) {
|
|
8508
9599
|
runtime.writeLine("Mend is stopped. No health claim was made.");
|
|
8509
9600
|
return;
|
|
@@ -8572,7 +9663,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
8572
9663
|
"Mend",
|
|
8573
9664
|
"Postgres",
|
|
8574
9665
|
...installation.config.bucket === "garage" ? ["Garage"] : [],
|
|
8575
|
-
...installation.config.edgeHost === void 0 ? [] : ["the edge"]
|
|
9666
|
+
...installation.config.edgeHost === void 0 ? [] : ["the edge"],
|
|
9667
|
+
...mirrorServices(installation.config.mirrors).map(
|
|
9668
|
+
(service) => service === "npm-mirror" ? "the npm mirror" : "the Docker mirror"
|
|
9669
|
+
)
|
|
8576
9670
|
];
|
|
8577
9671
|
runtime.writeLine(
|
|
8578
9672
|
`${stopped.slice(0, -1).join(", ")} and ${stopped.at(-1)} stopped. Volumes, configuration and workspace containers are retained.`
|
|
@@ -8615,9 +9709,51 @@ var nodeServerRuntime = () => {
|
|
|
8615
9709
|
writeLine: (line) => process.stdout.write(`${redactCredentials(line)}
|
|
8616
9710
|
`),
|
|
8617
9711
|
dockerDaemonFacts: hostDockerDaemonFacts,
|
|
8618
|
-
readLogin: (configDir) => savedLogin(configDir, environment)
|
|
9712
|
+
readLogin: (configDir) => savedLogin(configDir, environment),
|
|
9713
|
+
probeSsh: probeSshFromHere,
|
|
9714
|
+
readStdin: async () => {
|
|
9715
|
+
const chunks = [];
|
|
9716
|
+
for await (const chunk of process.stdin)
|
|
9717
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
9718
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9719
|
+
}
|
|
8619
9720
|
};
|
|
8620
9721
|
};
|
|
9722
|
+
var sshBannerAt = async (host, port, timeoutMs) => {
|
|
9723
|
+
const socket = net3.connect({ host, port });
|
|
9724
|
+
let deadline;
|
|
9725
|
+
try {
|
|
9726
|
+
return await Promise.race([
|
|
9727
|
+
new Promise((resolve8) => {
|
|
9728
|
+
socket.once(
|
|
9729
|
+
"data",
|
|
9730
|
+
(bytes) => resolve8(bytes.toString("utf8").split(/\r?\n/)[0]?.trim() ?? "")
|
|
9731
|
+
);
|
|
9732
|
+
socket.once("end", () => resolve8(null));
|
|
9733
|
+
socket.once("close", () => resolve8(null));
|
|
9734
|
+
socket.once("error", () => resolve8(null));
|
|
9735
|
+
}),
|
|
9736
|
+
new Promise((resolve8) => {
|
|
9737
|
+
deadline = setTimeout(() => resolve8(null), timeoutMs);
|
|
9738
|
+
})
|
|
9739
|
+
]);
|
|
9740
|
+
} finally {
|
|
9741
|
+
clearTimeout(deadline);
|
|
9742
|
+
socket.destroy();
|
|
9743
|
+
}
|
|
9744
|
+
};
|
|
9745
|
+
var probeSshFromHere = async (bind, port) => {
|
|
9746
|
+
const unspecified = bind === "0.0.0.0" || bind === "::";
|
|
9747
|
+
const addresses = unspecified ? Object.values(os10.networkInterfaces()).flatMap((entries) => entries ?? []).filter(
|
|
9748
|
+
(entry) => !entry.internal && (entry.family === "IPv4" || bind === "::" && !entry.address.startsWith("fe80"))
|
|
9749
|
+
).map((entry) => entry.address).slice(0, 4) : [bind];
|
|
9750
|
+
return Promise.all(
|
|
9751
|
+
addresses.map(async (address) => ({
|
|
9752
|
+
address,
|
|
9753
|
+
banner: await sshBannerAt(address, port, 3e3)
|
|
9754
|
+
}))
|
|
9755
|
+
);
|
|
9756
|
+
};
|
|
8621
9757
|
var savedLogin = (configDir, environment) => {
|
|
8622
9758
|
let saved = null;
|
|
8623
9759
|
try {
|
|
@@ -9782,6 +10918,7 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
9782
10918
|
appUrl: read.value.config.appUrl,
|
|
9783
10919
|
dockerContext: read.value.config.dockerContext,
|
|
9784
10920
|
edgeHost: read.value.config.edgeHost ?? null,
|
|
10921
|
+
mirrors: mirrorServices(read.value.config.mirrors),
|
|
9785
10922
|
generations: countEntries(path20.join(configDir, "generations")),
|
|
9786
10923
|
backups: countEntries(path20.join(configDir, "backups"))
|
|
9787
10924
|
};
|
|
@@ -9811,8 +10948,12 @@ var planLines = (plan, configDir) => {
|
|
|
9811
10948
|
`server Mend ${version}${appUrl === "" ? "" : ` at ${appUrl}`}${dockerContext === "" ? "" : ` \xB7 docker context ${dockerContext}`}`
|
|
9812
10949
|
);
|
|
9813
10950
|
if (dockerContext !== "") {
|
|
9814
|
-
const
|
|
9815
|
-
const
|
|
10951
|
+
const mirrors = plan.server.mirrors ?? [];
|
|
10952
|
+
const edge = [...edgeHost === null ? [] : ["edge"], ...mirrors];
|
|
10953
|
+
const edgeVolumes = [
|
|
10954
|
+
...edgeHost === null ? [] : ["mend-edge-data", "mend-edge-config"],
|
|
10955
|
+
...mirrors.map((service) => `mend-${service}`)
|
|
10956
|
+
];
|
|
9816
10957
|
lines.push(
|
|
9817
10958
|
` containers ${["mend", "postgres", "garage", ...edge].join(", ")} \xB7 volumes ${[MEND_DOCKER_NAMESPACE_WITH_GARAGE.store, ...secondaryVolumesOf(MEND_DOCKER_NAMESPACE_WITH_GARAGE), "mend-config", "mend-ssh", "mend-postgres", ...edgeVolumes].join(", ")} \xB7 image ghcr.io/sealant-sh/mend:${version}${edgeHost === null ? "" : ` \xB7 the edge for ${edgeHost}`}`
|
|
9818
10959
|
);
|
|
@@ -9871,7 +11012,9 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
9871
11012
|
`docker compose down failed: ${(down.error ?? down.stderr.trim()) || "no output"}. Containers and files are retained; fix Docker and run mend uninstall again.`
|
|
9872
11013
|
);
|
|
9873
11014
|
}
|
|
9874
|
-
server.writeLine(
|
|
11015
|
+
server.writeLine(
|
|
11016
|
+
`removed containers ${["mend", "postgres", "garage", ...mirrorServices(installation.config.mirrors)].join(", ")} and the Compose-owned volumes`
|
|
11017
|
+
);
|
|
9875
11018
|
const namespace = installation.config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
9876
11019
|
const dataVolumes = [namespace.store, ...secondaryVolumesOf(namespace)];
|
|
9877
11020
|
const identity = store.readIdentity();
|
|
@@ -10844,7 +11987,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
10844
11987
|
process.stdout.write("\x07");
|
|
10845
11988
|
}
|
|
10846
11989
|
};
|
|
10847
|
-
const
|
|
11990
|
+
const connect3 = async () => {
|
|
10848
11991
|
let url;
|
|
10849
11992
|
try {
|
|
10850
11993
|
url = await socketUrl(
|
|
@@ -10913,7 +12056,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
|
|
|
10913
12056
|
`
|
|
10914
12057
|
);
|
|
10915
12058
|
}
|
|
10916
|
-
const connected = await
|
|
12059
|
+
const connected = await connect3();
|
|
10917
12060
|
stopConnecting();
|
|
10918
12061
|
if (connected === "unavailable") return "unavailable";
|
|
10919
12062
|
if (connected === "aborted") return abortReason ?? "interrupted";
|
|
@@ -11320,8 +12463,9 @@ var printService = (config, service) => {
|
|
|
11320
12463
|
const status = service.status === "reachable" ? green7(service.status) : amber2(service.status);
|
|
11321
12464
|
const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
|
|
11322
12465
|
const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
|
|
12466
|
+
const processLine2 = service.processId === null ? "no process" : `process ${service.processId.slice(0, 8)}`;
|
|
11323
12467
|
say7(
|
|
11324
|
-
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim7(port.padEnd(7))} ${dim7(service.id.slice(0, 8))}`
|
|
12468
|
+
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim7(port.padEnd(7))} ${dim7(`service ${service.id.slice(0, 8)} \xB7 ${processLine2}`)}`
|
|
11325
12469
|
);
|
|
11326
12470
|
printServiceAccess(config, service);
|
|
11327
12471
|
printWorkspaceTtlFailure(service);
|
|
@@ -11364,8 +12508,30 @@ var serviceAdd = async (config, args) => {
|
|
|
11364
12508
|
say7(dim7(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
|
|
11365
12509
|
}
|
|
11366
12510
|
};
|
|
11367
|
-
var serviceList = async (config) => {
|
|
12511
|
+
var serviceList = async (config, args) => {
|
|
12512
|
+
const unknown = args.find((arg) => arg !== "--json");
|
|
12513
|
+
if (unknown !== void 0) {
|
|
12514
|
+
return fail7(`unknown argument ${unknown} \xB7 ${usageOf("service list")}`);
|
|
12515
|
+
}
|
|
11368
12516
|
const services = await fetchServices(config);
|
|
12517
|
+
if (args.includes("--json")) {
|
|
12518
|
+
printJson({
|
|
12519
|
+
version: 1,
|
|
12520
|
+
services: services.map((service) => ({
|
|
12521
|
+
id: service.id,
|
|
12522
|
+
name: service.label,
|
|
12523
|
+
sessionId: service.sessionId,
|
|
12524
|
+
processId: service.processId,
|
|
12525
|
+
status: service.status,
|
|
12526
|
+
workspacePort: service.workspacePort,
|
|
12527
|
+
protocol: service.protocol,
|
|
12528
|
+
hostPort: service.hostPort,
|
|
12529
|
+
authority: service.authority,
|
|
12530
|
+
browserUrl: service.browserUrl
|
|
12531
|
+
}))
|
|
12532
|
+
});
|
|
12533
|
+
return;
|
|
12534
|
+
}
|
|
11369
12535
|
if (services.length === 0) {
|
|
11370
12536
|
say7(dim7("no live services \u2014 mend service add <port> adopts a listening one"));
|
|
11371
12537
|
return;
|
|
@@ -11404,48 +12570,184 @@ var autoConnect = async (config, service) => {
|
|
|
11404
12570
|
say7(dim7(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
11405
12571
|
await tunnelServices(config, [service], null);
|
|
11406
12572
|
};
|
|
11407
|
-
var
|
|
12573
|
+
var SERVICE_WAIT_DEFAULT_MS = (() => {
|
|
11408
12574
|
const configured = Number(process.env["MEND_SERVICE_WAIT_MS"]);
|
|
11409
|
-
return Number.isFinite(configured) && configured > 0 ? configured :
|
|
12575
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 10 * 6e4;
|
|
11410
12576
|
})();
|
|
11411
|
-
var
|
|
11412
|
-
|
|
12577
|
+
var SERVICE_RUN_VALUED = /* @__PURE__ */ new Set(["--port", "--name", "--timeout"]);
|
|
12578
|
+
var serviceWaitOf = (head) => {
|
|
12579
|
+
const timeoutFlag = head.indexOf("--timeout");
|
|
12580
|
+
const timeoutText = timeoutFlag === -1 ? void 0 : head[timeoutFlag + 1];
|
|
12581
|
+
if (!head.includes("--wait")) {
|
|
12582
|
+
return timeoutFlag === -1 ? null : fail7("--timeout bounds --wait: pass --wait too, or leave --timeout out");
|
|
12583
|
+
}
|
|
12584
|
+
const timeoutMs = timeoutFlag === -1 ? SERVICE_WAIT_DEFAULT_MS : timeoutText === void 0 ? null : parseDuration(timeoutText);
|
|
12585
|
+
if (timeoutMs === null) return fail7(`${DURATION_ISSUE} \xB7 ${usageOf("service run")}`);
|
|
12586
|
+
return { deadline: clock.now() + timeoutMs, timeoutMs };
|
|
12587
|
+
};
|
|
12588
|
+
var serviceRunPositionals = (head) => head.filter(
|
|
12589
|
+
(arg, index) => !arg.startsWith("--") && !SERVICE_RUN_VALUED.has(head[index - 1] ?? "")
|
|
12590
|
+
);
|
|
12591
|
+
var failWith = (code, message) => {
|
|
12592
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
12593
|
+
`);
|
|
12594
|
+
return exitFlushed(code);
|
|
12595
|
+
};
|
|
12596
|
+
var withinServiceWait = async (work, wait) => {
|
|
12597
|
+
const bounded = await beforeDeadline(work, clock, wait?.deadline ?? null);
|
|
11413
12598
|
if (bounded.done) return bounded.value;
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
`
|
|
12599
|
+
return failWith(
|
|
12600
|
+
WAIT_TIMED_OUT,
|
|
12601
|
+
`Mend gave no answer within ${durationLine(wait?.timeoutMs ?? 0)} \xB7 nothing was started`
|
|
12602
|
+
);
|
|
12603
|
+
};
|
|
12604
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
12605
|
+
var exitCodeText = (exitCode) => exitCode === null ? "no exit code reported" : `code ${exitCode}`;
|
|
12606
|
+
var readServiceStart = async (config, sessionId) => {
|
|
12607
|
+
const [services, sessionStatus] = await Promise.all([
|
|
12608
|
+
request(config, "GET", "/services?all=1"),
|
|
12609
|
+
request(config, "GET", `/sessions/${sessionId}`).then(
|
|
12610
|
+
(detail) => detail.session.status,
|
|
12611
|
+
(error) => {
|
|
12612
|
+
if (error instanceof MendRequestError && error.kind === "http" && error.status === 404) {
|
|
12613
|
+
return null;
|
|
12614
|
+
}
|
|
12615
|
+
throw error;
|
|
12616
|
+
}
|
|
12617
|
+
)
|
|
12618
|
+
]);
|
|
12619
|
+
return { services, sessionStatus };
|
|
12620
|
+
};
|
|
12621
|
+
var reportAnswered = (config, service) => {
|
|
12622
|
+
const name = service.label;
|
|
12623
|
+
say7(`${green7("\u2713")} Service ${name} \xB7 ${service.status}`);
|
|
12624
|
+
printServiceEndpoint(config, service, false);
|
|
12625
|
+
say7(dim7(` logs: mend service logs ${name}`));
|
|
12626
|
+
if (willAutoConnect(config, service, false)) say7(dim7(` connect: mend service connect ${name}`));
|
|
12627
|
+
};
|
|
12628
|
+
var startServiceWaited = async (config, session, target, wait, spinner, start) => {
|
|
12629
|
+
const { name, port } = target;
|
|
12630
|
+
const startId = crypto.randomUUID();
|
|
12631
|
+
const after = durationLine(wait.timeoutMs);
|
|
12632
|
+
const stillStarting = (processId, more = "") => failWith(
|
|
12633
|
+
WAIT_TIMED_OUT,
|
|
12634
|
+
`${name} \xB7 still starting after ${after} \xB7 ${processId === null ? "" : `process ${processId.slice(0, 8)} runs \xB7 `}:${port} has not answered${more} \xB7 the Service keeps starting \xB7 mend logs --service ${name} --follow`
|
|
12635
|
+
);
|
|
12636
|
+
const ended = (processId, status, exitCode, more) => failWith(
|
|
12637
|
+
SERVICE_PROCESS_ENDED,
|
|
12638
|
+
`${name} \xB7 process ${processId.slice(0, 8)} ${status} \xB7 ${exitCodeText(exitCode)} \xB7 before :${port} answered \xB7 ${more}`
|
|
12639
|
+
);
|
|
12640
|
+
const posted = await withSpinner(
|
|
12641
|
+
spinner,
|
|
12642
|
+
beforeDeadline(
|
|
12643
|
+
start(startId).then(
|
|
12644
|
+
(view) => ({ ok: true, view }),
|
|
12645
|
+
(error) => ({ ok: false, error })
|
|
12646
|
+
),
|
|
12647
|
+
clock,
|
|
12648
|
+
wait.deadline
|
|
12649
|
+
)
|
|
11417
12650
|
);
|
|
11418
|
-
return
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
12651
|
+
if (!posted.done) return stillStarting(null, " \xB7 the start request has not returned");
|
|
12652
|
+
let attemptId = null;
|
|
12653
|
+
if (posted.value.ok) {
|
|
12654
|
+
const { view } = posted.value;
|
|
12655
|
+
attemptId = answeredAttemptId(view, startId);
|
|
12656
|
+
if (attemptId !== null) {
|
|
12657
|
+
const first = serviceStartStateOf(
|
|
12658
|
+
{ services: [view], sessionStatus: session.status },
|
|
12659
|
+
{ attemptId, sessionId: session.id, startId }
|
|
12660
|
+
);
|
|
12661
|
+
if (first.kind === "answered") return reportAnswered(config, flattenService(view));
|
|
12662
|
+
if (first.kind === "process-ended") {
|
|
12663
|
+
return ended(first.processId, first.status, first.exitCode, `mend logs --service ${name}`);
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
say7(
|
|
12667
|
+
`${amber2("\xB7")} ${name} \xB7 ${attemptId === null ? "started" : `process ${attemptId.slice(0, 8)} runs`} \xB7 :${port} has not answered yet \xB7 waiting up to ${after}; Mend probes the port every 20 s`
|
|
11426
12668
|
);
|
|
11427
|
-
}
|
|
11428
|
-
|
|
11429
|
-
|
|
12669
|
+
} else {
|
|
12670
|
+
const { error } = posted.value;
|
|
12671
|
+
if (!mayStillBeWorking(error)) {
|
|
12672
|
+
const read = await readWithin(
|
|
12673
|
+
() => request(config, "GET", "/services?all=1"),
|
|
12674
|
+
clock,
|
|
12675
|
+
wait.deadline
|
|
12676
|
+
).catch(() => null);
|
|
12677
|
+
const found = read?.done === true ? findStartAttempt(read.value, { attemptId: null, sessionId: session.id, startId }) : void 0;
|
|
12678
|
+
if (found !== void 0 && found.attempt.exitedAt !== null) {
|
|
12679
|
+
return ended(
|
|
12680
|
+
found.attempt.id,
|
|
12681
|
+
found.attempt.status,
|
|
12682
|
+
found.attempt.exitCode,
|
|
12683
|
+
errorText(error)
|
|
12684
|
+
);
|
|
12685
|
+
}
|
|
12686
|
+
return failWith(1, errorText(error));
|
|
12687
|
+
}
|
|
12688
|
+
say7(dim7(` the start got no answer (${errorText(error)}) \xB7 looking for the attempt it began\u2026`));
|
|
12689
|
+
}
|
|
12690
|
+
const started = clock.now();
|
|
12691
|
+
let line = `${name} \xB7 starting \xB7 waiting for :${port} to answer\u2026`;
|
|
12692
|
+
let nextReport = started + 6e4;
|
|
12693
|
+
const outcome = await withSpinner(
|
|
12694
|
+
() => line,
|
|
12695
|
+
waitForServiceStart({
|
|
12696
|
+
read: () => readServiceStart(config, session.id),
|
|
12697
|
+
target: { attemptId, sessionId: session.id, startId },
|
|
12698
|
+
deadline: wait.deadline,
|
|
12699
|
+
...clock,
|
|
12700
|
+
onStarting: (state) => {
|
|
12701
|
+
const running = state.processId === null ? "" : ` \xB7 process ${state.processId.slice(0, 8)} runs`;
|
|
12702
|
+
line = `${name} \xB7 starting${running} \xB7 :${port} has not answered`;
|
|
12703
|
+
if (chrome.isTTY !== true && clock.now() >= nextReport) {
|
|
12704
|
+
nextReport = clock.now() + 6e4;
|
|
12705
|
+
say7(dim7(` ${line} \xB7 ${durationLine(clock.now() - started)} so far`));
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
})
|
|
12709
|
+
).catch((error) => failFlushed(errorText(error)));
|
|
12710
|
+
switch (outcome.kind) {
|
|
12711
|
+
case "answered":
|
|
12712
|
+
return reportAnswered(config, flattenService(outcome.view));
|
|
12713
|
+
case "process-ended":
|
|
12714
|
+
return ended(
|
|
12715
|
+
outcome.processId,
|
|
12716
|
+
outcome.status,
|
|
12717
|
+
outcome.exitCode,
|
|
12718
|
+
`mend logs --service ${name}`
|
|
12719
|
+
);
|
|
12720
|
+
case "workspace-ended":
|
|
12721
|
+
return failWith(
|
|
12722
|
+
SERVICE_WORKSPACE_ENDED,
|
|
12723
|
+
`${name} \xB7 session ${session.id.slice(0, 8)} no longer exists, and its workspace went with it \xB7 before :${port} answered`
|
|
12724
|
+
);
|
|
12725
|
+
case "timeout":
|
|
12726
|
+
return stillStarting(outcome.last?.kind === "starting" ? outcome.last.processId : null);
|
|
12727
|
+
case "no-attempt":
|
|
12728
|
+
return failWith(
|
|
12729
|
+
1,
|
|
12730
|
+
`${name} \xB7 the start got no answer, and no attempt carrying its id appeared within a minute (a server older than this CLI stamps none) \xB7 mend service list`
|
|
12731
|
+
);
|
|
11430
12732
|
}
|
|
11431
12733
|
};
|
|
11432
12734
|
var serviceRun = async (config, args) => {
|
|
11433
12735
|
const dashdash = args.indexOf("--");
|
|
11434
12736
|
const usage2 = usageOf("service run");
|
|
11435
|
-
const
|
|
11436
|
-
const
|
|
12737
|
+
const head = dashdash === -1 ? args : args.slice(0, dashdash);
|
|
12738
|
+
const wait = serviceWaitOf(head);
|
|
11437
12739
|
if (dashdash === -1) {
|
|
11438
|
-
const positionals =
|
|
12740
|
+
const positionals = serviceRunPositionals(head);
|
|
11439
12741
|
const name2 = positionals.at(-1);
|
|
11440
12742
|
if (name2 === void 0) return fail7(usage2);
|
|
11441
12743
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
11442
12744
|
const session2 = await withinServiceWait(
|
|
11443
12745
|
resolveLiveSession(config, prefix2, "service run"),
|
|
11444
|
-
|
|
12746
|
+
wait
|
|
11445
12747
|
);
|
|
11446
12748
|
const recipes = await withinServiceWait(
|
|
11447
12749
|
api(config, "GET", `/sessions/${session2.id}/recipes`),
|
|
11448
|
-
|
|
12750
|
+
wait
|
|
11449
12751
|
);
|
|
11450
12752
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
11451
12753
|
if (recipe === void 0) {
|
|
@@ -11454,34 +12756,43 @@ var serviceRun = async (config, args) => {
|
|
|
11454
12756
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
11455
12757
|
);
|
|
11456
12758
|
}
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
12759
|
+
const startRecipe = (startId) => request(config, "POST", `/sessions/${session2.id}/services/recipe`, {
|
|
12760
|
+
name: recipe.name,
|
|
12761
|
+
startId
|
|
12762
|
+
});
|
|
12763
|
+
if (wait !== null) {
|
|
12764
|
+
if (recipe.protocol === "udp") {
|
|
12765
|
+
return fail7(`--wait needs a TCP port: ${recipe.name} is UDP, which has no probe`);
|
|
12766
|
+
}
|
|
12767
|
+
if (recipe.command === null) {
|
|
12768
|
+
return fail7(
|
|
12769
|
+
`--wait needs a command Mend starts: ${recipe.name} declares only a port, which Mend adopts with one probe \xB7 run it without --wait, or give the recipe a command`
|
|
12770
|
+
);
|
|
12771
|
+
}
|
|
12772
|
+
return startServiceWaited(
|
|
12773
|
+
config,
|
|
12774
|
+
session2,
|
|
12775
|
+
{ name: recipe.name, port: recipe.port },
|
|
12776
|
+
wait,
|
|
12777
|
+
`starting ${recipe.name} \u2014 waiting for :${recipe.port} to answer\u2026`,
|
|
12778
|
+
startRecipe
|
|
11463
12779
|
);
|
|
11464
12780
|
}
|
|
11465
12781
|
const service2 = await withSpinner(
|
|
11466
12782
|
recipe.command === null ? `adopting ${recipe.name} on :${recipe.port}\u2026` : recipe.protocol === "udp" ? `starting ${recipe.name} (udp :${recipe.port})\u2026` : `starting ${recipe.name} \u2014 waiting for :${recipe.port} to answer\u2026`,
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
}),
|
|
11471
|
-
deadline
|
|
11472
|
-
)
|
|
12783
|
+
mutateService(config, "POST", `/sessions/${session2.id}/services/recipe`, {
|
|
12784
|
+
name: recipe.name
|
|
12785
|
+
})
|
|
11473
12786
|
);
|
|
11474
|
-
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect")
|
|
12787
|
+
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
11475
12788
|
say7(`${green7("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
11476
12789
|
printServiceEndpoint(config, service2, tunneling2);
|
|
11477
12790
|
say7(dim7(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
11478
|
-
failUnlessAnswered(config, service2, wait);
|
|
11479
12791
|
if (tunneling2) await autoConnect(config, service2);
|
|
11480
12792
|
return;
|
|
11481
12793
|
}
|
|
11482
12794
|
const argv = args.slice(dashdash + 1);
|
|
11483
12795
|
if (argv.length === 0) return fail7(usage2);
|
|
11484
|
-
const head = args.slice(0, dashdash);
|
|
11485
12796
|
const portFlag = head.indexOf("--port");
|
|
11486
12797
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
11487
12798
|
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail7(usage2);
|
|
@@ -11493,34 +12804,33 @@ var serviceRun = async (config, args) => {
|
|
|
11493
12804
|
if (http && https) return fail7(usage2);
|
|
11494
12805
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
11495
12806
|
if (protocol === "udp" && browserScheme !== null) return fail7(usage2);
|
|
11496
|
-
if (protocol === "udp" && wait) {
|
|
12807
|
+
if (protocol === "udp" && wait !== null) {
|
|
11497
12808
|
return fail7("--wait needs a TCP port: UDP has no probe, so nothing could be waited for");
|
|
11498
12809
|
}
|
|
11499
|
-
const prefix = head
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
12810
|
+
const prefix = serviceRunPositionals(head)[0];
|
|
12811
|
+
const session = await withinServiceWait(resolveLiveSession(config, prefix, "service run"), wait);
|
|
12812
|
+
const body = { argv, port, name, protocol, browserScheme };
|
|
12813
|
+
if (wait !== null) {
|
|
12814
|
+
return startServiceWaited(
|
|
12815
|
+
config,
|
|
12816
|
+
session,
|
|
12817
|
+
{ name: name ?? argv[0] ?? "service", port },
|
|
12818
|
+
wait,
|
|
12819
|
+
`starting ${name ?? argv[0]} \u2014 waiting for :${port} to answer\u2026`,
|
|
12820
|
+
(startId) => request(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
12821
|
+
...body,
|
|
12822
|
+
startId
|
|
12823
|
+
})
|
|
12824
|
+
);
|
|
12825
|
+
}
|
|
11506
12826
|
const service = await withSpinner(
|
|
11507
12827
|
protocol === "udp" ? `starting ${name ?? argv[0]} (udp :${port})\u2026` : `starting ${name ?? argv[0]} \u2014 waiting for :${port} to answer\u2026`,
|
|
11508
|
-
|
|
11509
|
-
mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
11510
|
-
argv,
|
|
11511
|
-
port,
|
|
11512
|
-
name,
|
|
11513
|
-
protocol,
|
|
11514
|
-
browserScheme
|
|
11515
|
-
}),
|
|
11516
|
-
deadline
|
|
11517
|
-
)
|
|
12828
|
+
mutateService(config, "POST", `/sessions/${session.id}/services/run`, body)
|
|
11518
12829
|
);
|
|
11519
|
-
const tunneling = willAutoConnect(config, service, head.includes("--no-connect")
|
|
12830
|
+
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
11520
12831
|
say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
11521
12832
|
printServiceEndpoint(config, service, tunneling);
|
|
11522
12833
|
say7(dim7(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
11523
|
-
failUnlessAnswered(config, service, wait);
|
|
11524
12834
|
if (tunneling) await autoConnect(config, service);
|
|
11525
12835
|
};
|
|
11526
12836
|
var serviceLogs = async (config, args) => {
|
|
@@ -11748,8 +13058,9 @@ var serviceCommand = async (config, args) => {
|
|
|
11748
13058
|
case "connect":
|
|
11749
13059
|
return serviceConnect(config, rest);
|
|
11750
13060
|
case "list":
|
|
13061
|
+
return serviceList(config, rest);
|
|
11751
13062
|
case void 0:
|
|
11752
|
-
return serviceList(config);
|
|
13063
|
+
return serviceList(config, []);
|
|
11753
13064
|
case "logs":
|
|
11754
13065
|
return serviceLogs(config, rest);
|
|
11755
13066
|
case "restart":
|
|
@@ -12882,7 +14193,7 @@ var printJson = (value) => {
|
|
|
12882
14193
|
`);
|
|
12883
14194
|
};
|
|
12884
14195
|
var outputOnTerminal = false;
|
|
12885
|
-
var
|
|
14196
|
+
var recordedOutput = false;
|
|
12886
14197
|
var outputStopped = false;
|
|
12887
14198
|
var restoreTerminal = () => {
|
|
12888
14199
|
if (!outputOnTerminal || process.stdout.isTTY !== true) return;
|
|
@@ -12890,13 +14201,17 @@ var restoreTerminal = () => {
|
|
|
12890
14201
|
process.stdout.write(`${TERMINAL_MODES_RESET}\x1B[0m`);
|
|
12891
14202
|
};
|
|
12892
14203
|
process.once("exit", restoreTerminal);
|
|
14204
|
+
var onClosedPipe = (error) => {
|
|
14205
|
+
if (error.code !== "EPIPE") throw error;
|
|
14206
|
+
if (recordedOutput) return;
|
|
14207
|
+
process.exit(0);
|
|
14208
|
+
};
|
|
14209
|
+
process.stdout.on("error", onClosedPipe);
|
|
14210
|
+
process.stderr.on("error", onClosedPipe);
|
|
12893
14211
|
var writeOutput = (bytes) => {
|
|
12894
14212
|
if (outputStopped) return Promise.reject(new Error("stopped by a signal"));
|
|
12895
14213
|
if (process.stdout.isTTY === true) outputOnTerminal = true;
|
|
12896
|
-
|
|
12897
|
-
stdoutErrorsHandled = true;
|
|
12898
|
-
process.stdout.on("error", () => void 0);
|
|
12899
|
-
}
|
|
14214
|
+
recordedOutput = true;
|
|
12900
14215
|
return new Promise((resolve8, reject) => {
|
|
12901
14216
|
process.stdout.write(bytes, (error) => {
|
|
12902
14217
|
if (error === null || error === void 0) resolve8();
|
|
@@ -13076,26 +14391,57 @@ var resolveAnySession = async (config, word) => {
|
|
|
13076
14391
|
if ("error" in picked) return fail7(picked.error);
|
|
13077
14392
|
return picked.session;
|
|
13078
14393
|
};
|
|
14394
|
+
var serviceLogsTarget = async (config, needle, sessionWord) => {
|
|
14395
|
+
const session = sessionWord === null ? null : await resolveAnySession(config, sessionWord);
|
|
14396
|
+
const picked = pickServiceAttempt(
|
|
14397
|
+
await fetchServiceViews(config, true),
|
|
14398
|
+
needle,
|
|
14399
|
+
session?.id ?? null
|
|
14400
|
+
);
|
|
14401
|
+
if ("error" in picked) return fail7(picked.error);
|
|
14402
|
+
return { processId: picked.processId, again: `mend logs --service ${needle}` };
|
|
14403
|
+
};
|
|
13079
14404
|
var logsCommand = async (config, args) => {
|
|
13080
14405
|
chromeToStderr();
|
|
13081
14406
|
const parsed = parseLogsArgs(args);
|
|
13082
14407
|
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("logs")}`);
|
|
13083
|
-
const { session: word, follow, from, process: processPrefix } = parsed.args;
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
)
|
|
14408
|
+
const { session: word, follow, from, process: processPrefix, service } = parsed.args;
|
|
14409
|
+
let target;
|
|
14410
|
+
if (service !== null) {
|
|
14411
|
+
target = await serviceLogsTarget(config, service, word);
|
|
14412
|
+
} else {
|
|
14413
|
+
const session = word === null ? await resolveLiveSession(config, void 0, "logs") : await resolveAnySession(config, word);
|
|
14414
|
+
const id8 = session.id.slice(0, 8);
|
|
14415
|
+
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
14416
|
+
const picked = pickProcess(detail, processPrefix);
|
|
14417
|
+
if ("error" in picked) {
|
|
14418
|
+
const asService = processPrefix === null ? null : pickServiceAttempt(await fetchServiceViews(config, true), processPrefix, session.id);
|
|
14419
|
+
if (asService === null || "error" in asService && !asService.named) {
|
|
14420
|
+
return fail7(`session ${id8} \xB7 ${picked.error}`);
|
|
14421
|
+
}
|
|
14422
|
+
if ("error" in asService) return fail7(`session ${id8} \xB7 ${asService.error}`);
|
|
14423
|
+
target = {
|
|
14424
|
+
processId: asService.processId,
|
|
14425
|
+
again: `mend logs ${id8} --process ${processPrefix}`
|
|
14426
|
+
};
|
|
14427
|
+
} else {
|
|
14428
|
+
const chosen = picked.process;
|
|
14429
|
+
if (chosen.id === void 0 || chosen.sealantSessionId === null) {
|
|
14430
|
+
return fail7(
|
|
14431
|
+
`session ${id8} \xB7 this process has no recorded terminal (an adopted port, or a server older than process ids)`
|
|
14432
|
+
);
|
|
14433
|
+
}
|
|
14434
|
+
target = {
|
|
14435
|
+
processId: chosen.id,
|
|
14436
|
+
again: processPrefix === null ? `mend logs ${id8}` : `mend logs ${id8} --process ${processPrefix}`
|
|
14437
|
+
};
|
|
14438
|
+
}
|
|
13093
14439
|
}
|
|
13094
14440
|
stopOutputOnSignal(
|
|
13095
|
-
follow ? `stopped watching \xB7 the process keeps running \xB7
|
|
14441
|
+
follow ? `stopped watching \xB7 the process keeps running \xB7 ${target.again} --follow` : "stopped reading the record"
|
|
13096
14442
|
);
|
|
13097
14443
|
try {
|
|
13098
|
-
const last = await writeProcessLogs(config, target.
|
|
14444
|
+
const last = await writeProcessLogs(config, target.processId, from, follow);
|
|
13099
14445
|
restoreTerminal();
|
|
13100
14446
|
if (follow) say7(dim7(`
|
|
13101
14447
|
record ended \xB7 ${last.status} \xB7 next sequence ${last.next}`));
|
|
@@ -13112,7 +14458,7 @@ var waitCommand = async (config, args) => {
|
|
|
13112
14458
|
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("wait")}`);
|
|
13113
14459
|
const { session: word, timeoutMs, json: json2, process: processPrefix } = parsed.args;
|
|
13114
14460
|
const deadline = timeoutMs === null ? null : clock.now() + timeoutMs;
|
|
13115
|
-
const after =
|
|
14461
|
+
const after = durationLine(timeoutMs ?? 0);
|
|
13116
14462
|
const timedOut = async (session2, processId2, last) => {
|
|
13117
14463
|
say7(
|
|
13118
14464
|
session2 === null ? `${amber2("\xB7")} no session found within ${after}` : `${amber2("\xB7")} session ${session2.id.slice(0, 8)} \xB7 still running after ${after} \xB7 the command keeps running`
|
|
@@ -13674,7 +15020,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
13674
15020
|
);
|
|
13675
15021
|
process.exit(rerun.status ?? 0);
|
|
13676
15022
|
}
|
|
13677
|
-
const { runDashboard } = await import("./dashboard-
|
|
15023
|
+
const { runDashboard } = await import("./dashboard-RAZDVFR7.js");
|
|
13678
15024
|
const agentShare = await startShareIfBridge(config);
|
|
13679
15025
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
13680
15026
|
listServices: () => fetchServices(config),
|