@sealant/mend 0.36.0-next.665 → 0.36.0-next.684
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/README.md +19 -0
- package/dist/{chunk-K5WNRH4V.js → chunk-DHM7VXTR.js} +119 -14
- package/dist/{dashboard-DQB5RNXJ.js → dashboard-OBFWHSPD.js} +8 -2
- package/dist/main.js +1620 -206
- 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 +1 -1
- 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 +32 -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-keys-remove.1 +18 -0
- package/man/mend-ssh-keys.1 +15 -0
- package/man/mend-ssh-setup.1 +2 -2
- package/man/mend-ssh.1 +2 -2
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +2 -2
- 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 +10 -4
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -36,10 +36,12 @@ import {
|
|
|
36
36
|
hostUserNamespacesOf,
|
|
37
37
|
isDetachChunk,
|
|
38
38
|
isPasteChunk,
|
|
39
|
+
jsonWithoutCredentials,
|
|
39
40
|
landingFactLine,
|
|
40
41
|
landingFactsFromWire,
|
|
41
42
|
launcherNameOf,
|
|
42
43
|
matchProjectByCwd,
|
|
44
|
+
mayStillBeWorking,
|
|
43
45
|
narrowCredential,
|
|
44
46
|
noAnswerError,
|
|
45
47
|
normalizeGitAuthor,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
replaceConfirmLines,
|
|
52
54
|
repositoryCloneUrlIssue,
|
|
53
55
|
sameGrant,
|
|
56
|
+
serviceStartCorrelation,
|
|
54
57
|
servicesHoldOf,
|
|
55
58
|
sessionDisplayName,
|
|
56
59
|
sessionWorkspaceLines,
|
|
@@ -68,11 +71,12 @@ import {
|
|
|
68
71
|
workspaceRetirementLine,
|
|
69
72
|
worktreeJoinLine,
|
|
70
73
|
worktreeListsPeople
|
|
71
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-DHM7VXTR.js";
|
|
72
75
|
|
|
73
76
|
// src/main.ts
|
|
74
77
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
75
|
-
import * as
|
|
78
|
+
import * as crypto from "node:crypto";
|
|
79
|
+
import * as fs21 from "node:fs";
|
|
76
80
|
import { createRequire } from "node:module";
|
|
77
81
|
import * as net5 from "node:net";
|
|
78
82
|
import * as os13 from "node:os";
|
|
@@ -853,6 +857,421 @@ var EDGE_CADDYFILE = [
|
|
|
853
857
|
"}"
|
|
854
858
|
].join("\n") + "\n";
|
|
855
859
|
|
|
860
|
+
// src/server-mirrors.ts
|
|
861
|
+
var NPM_MIRROR_IMAGE = "nginx:1.29-alpine";
|
|
862
|
+
var DOCKER_MIRROR_IMAGE = "registry:3.1";
|
|
863
|
+
var MIRRORS_COMPOSE_FILE = "compose.mirrors.yaml";
|
|
864
|
+
var NPM_MIRROR_CONF_NAME = "npm-mirror.conf";
|
|
865
|
+
var NPM_MIRROR_URL = "http://npm-mirror:4873/";
|
|
866
|
+
var DOCKER_MIRROR_URL = "http://docker-mirror:5000";
|
|
867
|
+
var DOCKER_MIRROR_CONTAINER = "mend-docker-mirror";
|
|
868
|
+
var DEFAULT_NPM_MIRROR_MAX_SIZE = "10g";
|
|
869
|
+
var DOCKER_MIRROR_TTL = "168h";
|
|
870
|
+
var DEFAULT_DOCKER_MIRROR_MAX_SIZE = "20g";
|
|
871
|
+
var MIRROR_MIN_FREE = "5g";
|
|
872
|
+
var DOCKER_MIRROR_GUARD_NAME = "docker-mirror-guard.sh";
|
|
873
|
+
var dockerMirrorLogin = (docker2) => docker2 !== null && "upstreamUser" in docker2 ? docker2.upstreamUser : void 0;
|
|
874
|
+
var DEFAULT_MIRRORS = {
|
|
875
|
+
npm: { maxSize: DEFAULT_NPM_MIRROR_MAX_SIZE },
|
|
876
|
+
docker: { maxSize: DEFAULT_DOCKER_MIRROR_MAX_SIZE }
|
|
877
|
+
};
|
|
878
|
+
var parseMirrorSize = (value) => {
|
|
879
|
+
const match = /^([1-9]\d{0,5})([mMgG])$/.exec(value.trim());
|
|
880
|
+
if (match === null) return null;
|
|
881
|
+
const [, amount = "", unit = ""] = match;
|
|
882
|
+
const size = `${amount}${unit.toLowerCase()}`;
|
|
883
|
+
return mirrorSizeBytes(size) >= 1024 ** 3 ? size : null;
|
|
884
|
+
};
|
|
885
|
+
var mirrorSizeBytes = (size) => {
|
|
886
|
+
const amount = Number(size.slice(0, -1));
|
|
887
|
+
return size.endsWith("g") ? amount * 1024 ** 3 : amount * 1024 ** 2;
|
|
888
|
+
};
|
|
889
|
+
var CREDENTIAL = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,254}$/;
|
|
890
|
+
var isDockerHubCredential = (value) => CREDENTIAL.test(value);
|
|
891
|
+
var runsMirrors = (mirrors) => mirrors !== void 0 && (mirrors.npm !== null || mirrors.docker !== null);
|
|
892
|
+
var mirrorsEnvLines = (mirrors, dockerHubToken) => {
|
|
893
|
+
const user = dockerMirrorLogin(mirrors?.docker ?? null);
|
|
894
|
+
return [
|
|
895
|
+
...mirrors === void 0 || mirrors.npm === null ? [] : [`MEND_NPM_MIRROR_MAX_SIZE=${mirrors.npm.maxSize}`],
|
|
896
|
+
...mirrors === void 0 || mirrors.docker === null ? [] : [`MEND_DOCKER_MIRROR_MAX_SIZE=${mirrors.docker.maxSize}`],
|
|
897
|
+
...user === void 0 || dockerHubToken === void 0 ? [] : [`MEND_DOCKER_HUB_USERNAME=${user}`, `MEND_DOCKER_HUB_TOKEN=${dockerHubToken}`]
|
|
898
|
+
];
|
|
899
|
+
};
|
|
900
|
+
var LOGGING = [
|
|
901
|
+
" logging:",
|
|
902
|
+
" driver: json-file",
|
|
903
|
+
" options:",
|
|
904
|
+
' max-size: "20m"',
|
|
905
|
+
' max-file: "5"'
|
|
906
|
+
];
|
|
907
|
+
var HEALTH_TIMING = [
|
|
908
|
+
" interval: 10s",
|
|
909
|
+
" timeout: 5s",
|
|
910
|
+
" retries: 3",
|
|
911
|
+
" start_period: 10s"
|
|
912
|
+
];
|
|
913
|
+
var npmMirrorService = () => [
|
|
914
|
+
" # Read-through cache of registry.npmjs.org (npm-mirror.conf). Size-capped, and it leaves",
|
|
915
|
+
` # ${MIRROR_MIN_FREE} free on its disk; the least recently used tarballs go first. No credential sent.`,
|
|
916
|
+
" npm-mirror:",
|
|
917
|
+
` image: ${NPM_MIRROR_IMAGE}`,
|
|
918
|
+
" hostname: npm-mirror",
|
|
919
|
+
" restart: unless-stopped",
|
|
920
|
+
" environment:",
|
|
921
|
+
' NGINX_ENTRYPOINT_LOCAL_RESOLVERS: "1"',
|
|
922
|
+
' NGINX_ENTRYPOINT_QUIET_LOGS: "1"',
|
|
923
|
+
" NPM_MIRROR_MAX_SIZE: ${MEND_NPM_MIRROR_MAX_SIZE:?set MEND_NPM_MIRROR_MAX_SIZE in server.env}",
|
|
924
|
+
` NPM_MIRROR_MIN_FREE: ${MIRROR_MIN_FREE}`,
|
|
925
|
+
" volumes:",
|
|
926
|
+
` - ./${NPM_MIRROR_CONF_NAME}:/etc/nginx/templates/default.conf.template:ro`,
|
|
927
|
+
" - mend-npm-mirror:/var/cache/npm-mirror",
|
|
928
|
+
" # Answered by nginx itself, so it says the mirror is up without asking the registry.",
|
|
929
|
+
" healthcheck:",
|
|
930
|
+
' test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:4873/-/ping"]',
|
|
931
|
+
...HEALTH_TIMING,
|
|
932
|
+
...LOGGING
|
|
933
|
+
];
|
|
934
|
+
var dockerMirrorService = (mirrors) => [
|
|
935
|
+
" # Pull-through cache of Docker Hub, run by its guard (docker-mirror-guard.sh): the cache stays",
|
|
936
|
+
` # under its cap and leaves ${MIRROR_MIN_FREE} free on its disk. Layer data is deleted seven days after a`,
|
|
937
|
+
" # fetch. The metrics listener stays on the container's loopback.",
|
|
938
|
+
" docker-mirror:",
|
|
939
|
+
` image: ${DOCKER_MIRROR_IMAGE}`,
|
|
940
|
+
` container_name: ${DOCKER_MIRROR_CONTAINER}`,
|
|
941
|
+
" hostname: docker-mirror",
|
|
942
|
+
" restart: unless-stopped",
|
|
943
|
+
" init: true",
|
|
944
|
+
` entrypoint: ["/bin/sh", "/mend/${DOCKER_MIRROR_GUARD_NAME}"]`,
|
|
945
|
+
" environment:",
|
|
946
|
+
" DOCKER_MIRROR_MAX_SIZE:",
|
|
947
|
+
" ${MEND_DOCKER_MIRROR_MAX_SIZE:?set MEND_DOCKER_MIRROR_MAX_SIZE in server.env}",
|
|
948
|
+
` DOCKER_MIRROR_MIN_FREE: ${MIRROR_MIN_FREE}`,
|
|
949
|
+
" REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io",
|
|
950
|
+
` REGISTRY_PROXY_TTL: ${DOCKER_MIRROR_TTL}`,
|
|
951
|
+
" REGISTRY_LOG_LEVEL: warn",
|
|
952
|
+
' REGISTRY_LOG_ACCESSLOG_DISABLED: "true"',
|
|
953
|
+
" REGISTRY_HTTP_DEBUG_ADDR: 127.0.0.1:5001",
|
|
954
|
+
' REGISTRY_HTTP_DEBUG_PROMETHEUS_ENABLED: "true"',
|
|
955
|
+
...dockerMirrorLogin(mirrors.docker) === void 0 ? [] : [
|
|
956
|
+
" # A token the operator declared Public Repo Read-only: every session can pull what it reads.",
|
|
957
|
+
" REGISTRY_PROXY_USERNAME: ${MEND_DOCKER_HUB_USERNAME:?set MEND_DOCKER_HUB_USERNAME in server.env}",
|
|
958
|
+
" REGISTRY_PROXY_PASSWORD: ${MEND_DOCKER_HUB_TOKEN:?set MEND_DOCKER_HUB_TOKEN in server.env}"
|
|
959
|
+
],
|
|
960
|
+
" volumes:",
|
|
961
|
+
` - ./${DOCKER_MIRROR_GUARD_NAME}:/mend/${DOCKER_MIRROR_GUARD_NAME}:ro`,
|
|
962
|
+
" - mend-docker-mirror:/var/lib/registry",
|
|
963
|
+
" # Healthy while its guard keeps working, which it records every pass. The registry inside may",
|
|
964
|
+
" # be paused for want of disk, or not started because Docker Hub did not answer (it asks Hub as",
|
|
965
|
+
" # it starts, and the guard tries again every pass): setup does not wait for it.",
|
|
966
|
+
" healthcheck:",
|
|
967
|
+
` test: ["CMD-SHELL", 'test -n "$$(find /tmp/mend-mirror-guard -mmin -2)"']`,
|
|
968
|
+
...HEALTH_TIMING,
|
|
969
|
+
...LOGGING
|
|
970
|
+
];
|
|
971
|
+
var renderMirrorsOverlay = (mirrors) => {
|
|
972
|
+
if (!runsMirrors(mirrors)) return void 0;
|
|
973
|
+
return [
|
|
974
|
+
"# Written by mend server setup: the package and image mirrors this install runs, on the Compose",
|
|
975
|
+
"# network only (no host port). Values come from server.env. Sessions install npm packages and",
|
|
976
|
+
"# pull Docker Hub images through them; a mirror that is down sends them to the registry itself.",
|
|
977
|
+
"services:",
|
|
978
|
+
" mend:",
|
|
979
|
+
" environment:",
|
|
980
|
+
...mirrors.npm === null ? [] : [` MEND_NPM_MIRROR_URL: ${NPM_MIRROR_URL}`],
|
|
981
|
+
...mirrors.docker === null ? [] : [
|
|
982
|
+
` SEALANT_DOCKER_REGISTRY_MIRRORS: ${DOCKER_MIRROR_URL}`,
|
|
983
|
+
` SEALANT_DOCKER_REGISTRY_MIRROR_CONTAINER: ${DOCKER_MIRROR_CONTAINER}`
|
|
984
|
+
],
|
|
985
|
+
"",
|
|
986
|
+
...mirrors.npm === null ? [] : [...npmMirrorService(), ""],
|
|
987
|
+
...mirrors.docker === null ? [] : [...dockerMirrorService(mirrors), ""],
|
|
988
|
+
"volumes:",
|
|
989
|
+
...mirrors.npm === null ? [] : [" mend-npm-mirror:"],
|
|
990
|
+
...mirrors.docker === null ? [] : [" mend-docker-mirror:"],
|
|
991
|
+
""
|
|
992
|
+
].join("\n");
|
|
993
|
+
};
|
|
994
|
+
var mirrorServices = (mirrors) => [
|
|
995
|
+
...mirrors === void 0 || mirrors.npm === null ? [] : ["npm-mirror"],
|
|
996
|
+
...mirrors === void 0 || mirrors.docker === null ? [] : ["docker-mirror"]
|
|
997
|
+
];
|
|
998
|
+
var mirrorImagesOf = (overlay) => [...overlay.matchAll(/^ {4}image:[ \t]*((?:nginx|registry):[^\s#]+)[ \t]*$/gm)].map((match) => match[1]).filter((image) => image !== void 0);
|
|
999
|
+
var NPM_MIRROR_CONF = `# Mend's npm mirror: a read-through cache of the public npm registry (registry.npmjs.org) for the
|
|
1000
|
+
# workspaces on this host. nginx keeps what it fetched and serves it again. The package manager
|
|
1001
|
+
# checks every tarball against its lockfile's integrity hash, so the cache never decides what is
|
|
1002
|
+
# installed. Nothing is published through it, and no credential is sent upstream. When the
|
|
1003
|
+
# registry fails, a copy already held is served instead.
|
|
1004
|
+
proxy_cache_path /var/cache/npm-mirror levels=2:2 keys_zone=npm:64m
|
|
1005
|
+
max_size=\${NPM_MIRROR_MAX_SIZE} min_free=\${NPM_MIRROR_MIN_FREE}
|
|
1006
|
+
inactive=90d use_temp_path=off;
|
|
1007
|
+
|
|
1008
|
+
# Abbreviated metadata (what installs ask for) and full metadata are different documents.
|
|
1009
|
+
map $http_accept $npm_document {
|
|
1010
|
+
"~application/vnd\\.npm\\.install-v1\\+json" abbreviated;
|
|
1011
|
+
default full;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
# One JSON line per request: \`mend server status\` reads the cache field of the tarball lines.
|
|
1015
|
+
log_format npm_mirror escape=json
|
|
1016
|
+
'{"time":"$time_iso8601","method":"$request_method","uri":"$uri","status":$status,'
|
|
1017
|
+
'"cache":"$upstream_cache_status","bytes":$body_bytes_sent}';
|
|
1018
|
+
|
|
1019
|
+
server {
|
|
1020
|
+
listen 4873;
|
|
1021
|
+
access_log /dev/stdout npm_mirror;
|
|
1022
|
+
error_log /dev/stderr warn;
|
|
1023
|
+
client_max_body_size 8m;
|
|
1024
|
+
|
|
1025
|
+
resolver \${NGINX_LOCAL_RESOLVERS} valid=60s ipv6=off;
|
|
1026
|
+
resolver_timeout 5s;
|
|
1027
|
+
set $npm_registry registry.npmjs.org;
|
|
1028
|
+
|
|
1029
|
+
proxy_http_version 1.1;
|
|
1030
|
+
proxy_set_header Host registry.npmjs.org;
|
|
1031
|
+
proxy_set_header Connection "";
|
|
1032
|
+
# Never forward a credential: the mirror serves the public registry, the same to everyone.
|
|
1033
|
+
proxy_set_header Authorization "";
|
|
1034
|
+
proxy_set_header Cookie "";
|
|
1035
|
+
# Uncompressed from upstream, so one cached copy serves every client; gzip below compresses it.
|
|
1036
|
+
proxy_set_header Accept-Encoding "";
|
|
1037
|
+
proxy_ssl_server_name on;
|
|
1038
|
+
proxy_ssl_name registry.npmjs.org;
|
|
1039
|
+
proxy_ssl_verify on;
|
|
1040
|
+
proxy_ssl_verify_depth 4;
|
|
1041
|
+
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
|
1042
|
+
proxy_connect_timeout 10s;
|
|
1043
|
+
proxy_read_timeout 60s;
|
|
1044
|
+
proxy_ignore_headers Cache-Control Expires Set-Cookie Vary;
|
|
1045
|
+
proxy_hide_header Set-Cookie;
|
|
1046
|
+
proxy_cache_lock on;
|
|
1047
|
+
proxy_cache_lock_timeout 120s;
|
|
1048
|
+
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_429;
|
|
1049
|
+
proxy_cache_background_update on;
|
|
1050
|
+
add_header X-Mend-Mirror $upstream_cache_status always;
|
|
1051
|
+
|
|
1052
|
+
gzip on;
|
|
1053
|
+
gzip_proxied any;
|
|
1054
|
+
gzip_types application/json application/vnd.npm.install-v1+json;
|
|
1055
|
+
|
|
1056
|
+
# Answered here, so a client can tell the mirror is up without asking the registry.
|
|
1057
|
+
location = /-/ping {
|
|
1058
|
+
default_type application/json;
|
|
1059
|
+
return 200 '{}';
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
# Tarballs never change once published: kept until the cache needs the room.
|
|
1063
|
+
location ~ ^/(?:@[^/]+/)?[^/]+/-/[^/]+\\.tgz$ {
|
|
1064
|
+
limit_except GET HEAD { deny all; }
|
|
1065
|
+
proxy_cache npm;
|
|
1066
|
+
proxy_cache_key $uri;
|
|
1067
|
+
proxy_cache_valid 200 365d;
|
|
1068
|
+
proxy_pass https://$npm_registry;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
# Audit reports go to the registry as they are, never cached.
|
|
1072
|
+
location /-/npm/v1/security/ {
|
|
1073
|
+
limit_except POST { deny all; }
|
|
1074
|
+
proxy_pass https://$npm_registry;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
# Metadata: fresh for five minutes, then revalidated; served stale while the registry fails.
|
|
1078
|
+
# The key is the whole request, query included: a search for one package never answers another.
|
|
1079
|
+
location / {
|
|
1080
|
+
limit_except GET HEAD { deny all; }
|
|
1081
|
+
proxy_cache npm;
|
|
1082
|
+
proxy_cache_key $request_uri|$npm_document;
|
|
1083
|
+
proxy_cache_valid 200 5m;
|
|
1084
|
+
proxy_cache_valid 404 1m;
|
|
1085
|
+
proxy_cache_revalidate on;
|
|
1086
|
+
proxy_pass https://$npm_registry;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
`;
|
|
1090
|
+
var DOCKER_MIRROR_GUARD = `#!/bin/sh
|
|
1091
|
+
# Mend's Docker mirror guard (written by mend server setup): runs the registry, and keeps its cache
|
|
1092
|
+
# under a byte cap and the disk it lives on above a floor. Over the cap, the cache is cleared and
|
|
1093
|
+
# fills again from Docker Hub. Below the floor, the cache is cleared and the registry stays stopped
|
|
1094
|
+
# until there is room again; meanwhile session Docker daemons pull from Docker Hub directly. The
|
|
1095
|
+
# registry has no size cap of its own, and its seven-day expiry deletes layer data only.
|
|
1096
|
+
set -u
|
|
1097
|
+
root=/var/lib/registry
|
|
1098
|
+
state=/tmp/mend-mirror-guard
|
|
1099
|
+
size_mb() {
|
|
1100
|
+
case "$1" in
|
|
1101
|
+
*g) echo $((\${1%g} * 1024)) ;;
|
|
1102
|
+
*m) echo "\${1%m}" ;;
|
|
1103
|
+
*) echo "mend docker mirror guard: cannot read size $1" >&2 && exit 64 ;;
|
|
1104
|
+
esac
|
|
1105
|
+
}
|
|
1106
|
+
cap=$(size_mb "\${DOCKER_MIRROR_MAX_SIZE:?}") || exit 64
|
|
1107
|
+
floor=$(size_mb "\${DOCKER_MIRROR_MIN_FREE:?}") || exit 64
|
|
1108
|
+
interval=\${DOCKER_MIRROR_GUARD_INTERVAL:-30}
|
|
1109
|
+
pid=
|
|
1110
|
+
stop_registry() {
|
|
1111
|
+
if [ -n "$pid" ]; then
|
|
1112
|
+
kill "$pid" 2>/dev/null
|
|
1113
|
+
wait "$pid" 2>/dev/null
|
|
1114
|
+
fi
|
|
1115
|
+
pid=
|
|
1116
|
+
}
|
|
1117
|
+
clear_cache() { rm -rf "$root/docker" "$root/scheduler-state.json"; }
|
|
1118
|
+
cache_held() { [ -e "$root/docker" ] || [ -e "$root/scheduler-state.json" ]; }
|
|
1119
|
+
trap 'stop_registry; exit 0' TERM INT
|
|
1120
|
+
while :; do
|
|
1121
|
+
free=$(df -Pm "$root" | awk 'NR == 2 { print $4 }')
|
|
1122
|
+
if [ "$free" -lt "$floor" ]; then
|
|
1123
|
+
# Below the floor: no registry, and no cache, whether the registry was running or this is a
|
|
1124
|
+
# start with a cache left from before. Once the space is back, the next pass starts it again.
|
|
1125
|
+
stop_registry
|
|
1126
|
+
# What is left, looked at again after clearing: the log and the status say what this finds,
|
|
1127
|
+
# not what was attempted.
|
|
1128
|
+
if cache_held; then
|
|
1129
|
+
clear_cache
|
|
1130
|
+
if cache_held; then outcome="cache could not be cleared"; else outcome="cache cleared"; fi
|
|
1131
|
+
echo "mend docker mirror guard: \${free} MiB free on its disk, below \${floor} MiB: \${outcome}, registry paused" >&2
|
|
1132
|
+
fi
|
|
1133
|
+
if cache_held; then held=kept; else held=none; fi
|
|
1134
|
+
echo "paused \${free} \${floor} \${held}" >"$state"
|
|
1135
|
+
else
|
|
1136
|
+
used=$(du -sm "$root" | cut -f1)
|
|
1137
|
+
if [ "$used" -gt "$cap" ]; then
|
|
1138
|
+
stop_registry
|
|
1139
|
+
clear_cache
|
|
1140
|
+
if cache_held; then outcome="could not be cleared"; else outcome="cleared"; fi
|
|
1141
|
+
echo "mend docker mirror guard: cache \${used} MiB, over its \${cap} MiB cap: \${outcome}" >&2
|
|
1142
|
+
fi
|
|
1143
|
+
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
|
|
1144
|
+
registry serve /etc/distribution/config.yml &
|
|
1145
|
+
pid=$!
|
|
1146
|
+
fi
|
|
1147
|
+
echo "running" >"$state"
|
|
1148
|
+
fi
|
|
1149
|
+
sleep "$interval" &
|
|
1150
|
+
wait $!
|
|
1151
|
+
done
|
|
1152
|
+
`;
|
|
1153
|
+
var formatBytes = (bytes) => {
|
|
1154
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1155
|
+
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
1156
|
+
let value = bytes / 1024;
|
|
1157
|
+
let unit = 0;
|
|
1158
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1159
|
+
value /= 1024;
|
|
1160
|
+
unit += 1;
|
|
1161
|
+
}
|
|
1162
|
+
return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
|
|
1163
|
+
};
|
|
1164
|
+
var percent = (part, whole) => whole === 0 ? "" : ` (${Math.round(part / whole * 100)}%)`;
|
|
1165
|
+
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`;
|
|
1166
|
+
var mirrorDiskOf = (stdout) => {
|
|
1167
|
+
const [used, free, guard = ""] = stdout.trim().split("\n").map((line) => line.trim());
|
|
1168
|
+
const usedKiB = Number(used);
|
|
1169
|
+
const freeKiB = Number(free);
|
|
1170
|
+
if (used === void 0 || free === void 0 || used === "" || free === "") return null;
|
|
1171
|
+
if (!Number.isFinite(usedKiB) || !Number.isFinite(freeKiB)) return null;
|
|
1172
|
+
const paused = /^paused (\d+) (\d+)(?: (none|kept))?$/.exec(guard);
|
|
1173
|
+
return {
|
|
1174
|
+
used: usedKiB * 1024,
|
|
1175
|
+
free: freeKiB * 1024,
|
|
1176
|
+
guard: paused?.[1] !== void 0 && paused[2] !== void 0 ? {
|
|
1177
|
+
state: "paused",
|
|
1178
|
+
freeMiB: Number(paused[1]),
|
|
1179
|
+
floorMiB: Number(paused[2]),
|
|
1180
|
+
cache: paused[3] === "none" || paused[3] === "kept" ? paused[3] : null
|
|
1181
|
+
} : guard === "running" ? { state: "running" } : null
|
|
1182
|
+
};
|
|
1183
|
+
};
|
|
1184
|
+
var FROM_CACHE = /* @__PURE__ */ new Set(["HIT", "STALE", "UPDATING", "REVALIDATED"]);
|
|
1185
|
+
var FROM_UPSTREAM = /* @__PURE__ */ new Set(["MISS", "EXPIRED", "BYPASS"]);
|
|
1186
|
+
var npmMirrorTraffic = (log) => {
|
|
1187
|
+
let tarballs = 0;
|
|
1188
|
+
let fromCache = 0;
|
|
1189
|
+
let fromRegistry = 0;
|
|
1190
|
+
for (const line of log.split("\n")) {
|
|
1191
|
+
const start = line.indexOf("{");
|
|
1192
|
+
if (start < 0) continue;
|
|
1193
|
+
let fields;
|
|
1194
|
+
try {
|
|
1195
|
+
fields = JSON.parse(line.slice(start));
|
|
1196
|
+
} catch {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof fields !== "object" || fields === null) continue;
|
|
1200
|
+
const entry = new Map(Object.entries(fields));
|
|
1201
|
+
const uri = entry.get("uri");
|
|
1202
|
+
const cache = entry.get("cache");
|
|
1203
|
+
if (typeof uri !== "string" || !uri.endsWith(".tgz") || typeof cache !== "string") continue;
|
|
1204
|
+
tarballs += 1;
|
|
1205
|
+
if (FROM_CACHE.has(cache)) fromCache += 1;
|
|
1206
|
+
else if (FROM_UPSTREAM.has(cache)) fromRegistry += 1;
|
|
1207
|
+
}
|
|
1208
|
+
return { tarballs, fromCache, fromRegistry };
|
|
1209
|
+
};
|
|
1210
|
+
var dockerMirrorTraffic = (metrics) => {
|
|
1211
|
+
const counter = (name, type2) => {
|
|
1212
|
+
const match = new RegExp(`^registry_proxy_${name}_total\\{type="${type2}"\\} (\\d+)`, "m").exec(
|
|
1213
|
+
metrics
|
|
1214
|
+
);
|
|
1215
|
+
return match?.[1] === void 0 ? null : Number(match[1]);
|
|
1216
|
+
};
|
|
1217
|
+
const values = [
|
|
1218
|
+
counter("hits", "blob"),
|
|
1219
|
+
counter("misses", "blob"),
|
|
1220
|
+
counter("hits", "manifest"),
|
|
1221
|
+
counter("misses", "manifest")
|
|
1222
|
+
];
|
|
1223
|
+
const [blobHits, blobMisses, manifestHits, manifestMisses] = values;
|
|
1224
|
+
if (blobHits === void 0 || blobHits === null || blobMisses === void 0 || blobMisses === null || manifestHits === void 0 || manifestHits === null || manifestMisses === void 0 || manifestMisses === null) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
blobs: { hits: blobHits, misses: blobMisses },
|
|
1229
|
+
manifests: { hits: manifestHits, misses: manifestMisses }
|
|
1230
|
+
};
|
|
1231
|
+
};
|
|
1232
|
+
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`;
|
|
1233
|
+
var observedNpmMirrorLine = (mirrors, observed) => {
|
|
1234
|
+
if (mirrors.npm === null)
|
|
1235
|
+
return "npm mirror \xB7 off on this install \xB7 mend server setup --npm-mirror turns it on";
|
|
1236
|
+
if (!observed.running)
|
|
1237
|
+
return "npm mirror \xB7 container not running \xB7 sessions install from registry.npmjs.org directly";
|
|
1238
|
+
const { traffic } = observed;
|
|
1239
|
+
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`;
|
|
1240
|
+
return [
|
|
1241
|
+
"npm mirror \xB7 running",
|
|
1242
|
+
diskWords(observed.disk, mirrors.npm.maxSize),
|
|
1243
|
+
seen,
|
|
1244
|
+
"observed"
|
|
1245
|
+
].join(" \xB7 ");
|
|
1246
|
+
};
|
|
1247
|
+
var observedDockerMirrorLine = (mirrors, observed) => {
|
|
1248
|
+
if (mirrors.docker === null)
|
|
1249
|
+
return "docker mirror \xB7 off on this install \xB7 mend server setup --docker-mirror turns it on";
|
|
1250
|
+
if (!observed.running)
|
|
1251
|
+
return "docker mirror \xB7 container not running \xB7 session Docker daemons pull from Docker Hub directly";
|
|
1252
|
+
const { disk, traffic } = observed;
|
|
1253
|
+
if (typeof disk !== "string" && disk.guard?.state === "paused")
|
|
1254
|
+
return [
|
|
1255
|
+
"docker mirror \xB7 paused by its disk guard",
|
|
1256
|
+
`${formatBytes(disk.guard.freeMiB * 1024 ** 2)} free on its disk, below ${formatBytes(disk.guard.floorMiB * 1024 ** 2)}`,
|
|
1257
|
+
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",
|
|
1258
|
+
"session Docker daemons pull from Docker Hub directly until there is room",
|
|
1259
|
+
"observed"
|
|
1260
|
+
].join(" \xB7 ");
|
|
1261
|
+
const since = observed.startedAt === null ? "since it started" : `since ${observed.startedAt}`;
|
|
1262
|
+
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`;
|
|
1263
|
+
const user = dockerMirrorLogin(mirrors.docker);
|
|
1264
|
+
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`;
|
|
1265
|
+
return [
|
|
1266
|
+
"docker mirror \xB7 running",
|
|
1267
|
+
diskWords(disk, mirrors.docker.maxSize),
|
|
1268
|
+
"layers evicted 7 days after each fetch",
|
|
1269
|
+
seen,
|
|
1270
|
+
login2,
|
|
1271
|
+
"observed"
|
|
1272
|
+
].join(" \xB7 ");
|
|
1273
|
+
};
|
|
1274
|
+
|
|
856
1275
|
// src/server-edge.ts
|
|
857
1276
|
var EXPOSURES = ["loopback", "private", "public"];
|
|
858
1277
|
var TENANCIES = ["single", "multi"];
|
|
@@ -909,7 +1328,8 @@ var renderPostureOverlay = (posture) => {
|
|
|
909
1328
|
};
|
|
910
1329
|
var composeOverlays = (posture) => [
|
|
911
1330
|
...posture.edgeHost === void 0 ? [] : [EDGE_COMPOSE_FILE],
|
|
912
|
-
...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE]
|
|
1331
|
+
...renderPostureOverlay(posture) === void 0 ? [] : [POSTURE_COMPOSE_FILE],
|
|
1332
|
+
...runsMirrors(posture.mirrors) ? [MIRRORS_COMPOSE_FILE] : []
|
|
913
1333
|
];
|
|
914
1334
|
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? new Map(Object.entries(value)) : null;
|
|
915
1335
|
var healthPosture = (body) => {
|
|
@@ -1980,7 +2400,7 @@ var COMMANDS = [
|
|
|
1980
2400
|
"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.",
|
|
1981
2401
|
"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.",
|
|
1982
2402
|
"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.",
|
|
1983
|
-
"The platform takes at most 64 words,
|
|
2403
|
+
"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.",
|
|
1984
2404
|
"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).",
|
|
1985
2405
|
"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."
|
|
1986
2406
|
],
|
|
@@ -2005,31 +2425,39 @@ var COMMANDS = [
|
|
|
2005
2425
|
name: "logs",
|
|
2006
2426
|
section: "sessions",
|
|
2007
2427
|
summary: "print a session's recorded terminal output",
|
|
2008
|
-
synopsis: [
|
|
2428
|
+
synopsis: [
|
|
2429
|
+
"[session] [--follow] [--from <sequence>] [--process <id>]",
|
|
2430
|
+
"[session] --service <name-or-id> [--follow] [--from <sequence>]"
|
|
2431
|
+
],
|
|
2009
2432
|
description: [
|
|
2010
2433
|
"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.",
|
|
2011
|
-
"<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."
|
|
2434
|
+
"<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.",
|
|
2435
|
+
"--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."
|
|
2012
2436
|
],
|
|
2013
2437
|
options: [
|
|
2014
2438
|
{ flag: "--follow, -f", text: "keep printing until the process ends" },
|
|
2015
2439
|
{ flag: "--from <sequence>", text: "start at a record sequence. Default: 0" },
|
|
2016
|
-
{ flag: "--process <id>", text: "another process of the session, by a prefix of its id" }
|
|
2440
|
+
{ flag: "--process <id>", text: "another process of the session, by a prefix of its id" },
|
|
2441
|
+
{ flag: "--service <name-or-id>", text: "a Service's current attempt" }
|
|
2442
|
+
],
|
|
2443
|
+
examples: [
|
|
2444
|
+
{ command: "mend logs 3f2a --follow", text: "" },
|
|
2445
|
+
{ command: "mend logs --service web --follow", text: "the Service web's output, live" }
|
|
2017
2446
|
],
|
|
2018
|
-
examples: [{ command: "mend logs 3f2a --follow", text: "" }],
|
|
2019
2447
|
see: ["run", "wait", "service logs"]
|
|
2020
2448
|
},
|
|
2021
2449
|
{
|
|
2022
2450
|
name: "wait",
|
|
2023
2451
|
section: "sessions",
|
|
2024
2452
|
summary: "wait for a session's command to end",
|
|
2025
|
-
synopsis: ["[session] [--timeout <
|
|
2453
|
+
synopsis: ["[session] [--timeout <duration>] [--process <id>] [--json]"],
|
|
2026
2454
|
description: [
|
|
2027
2455
|
"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.",
|
|
2028
|
-
"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.",
|
|
2456
|
+
"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.",
|
|
2029
2457
|
"<session> is the session id, a prefix of it, or the worktree's name. With none, the one live session is taken."
|
|
2030
2458
|
],
|
|
2031
2459
|
options: [
|
|
2032
|
-
{ flag: "--timeout <
|
|
2460
|
+
{ flag: "--timeout <duration>", text: "give up after this long (90, 90s, 5m, 1h); exit 124" },
|
|
2033
2461
|
{
|
|
2034
2462
|
flag: "--process <id>",
|
|
2035
2463
|
text: "wait for this process of the session, by a prefix of its id"
|
|
@@ -2230,18 +2658,22 @@ var COMMANDS = [
|
|
|
2230
2658
|
section: "services",
|
|
2231
2659
|
summary: "start and supervise a server in the session's workspace",
|
|
2232
2660
|
synopsis: [
|
|
2233
|
-
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait] [--no-connect] -- <command...>",
|
|
2234
|
-
"[session] <name> [--wait] [--no-connect]"
|
|
2661
|
+
"[session] --port <port> [--name <n>] [--udp] [--http|--https] [--wait [--timeout <duration>]] [--no-connect] -- <command...>",
|
|
2662
|
+
"[session] <name> [--wait [--timeout <duration>]] [--no-connect]"
|
|
2235
2663
|
],
|
|
2236
2664
|
description: [
|
|
2237
2665
|
"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.",
|
|
2238
|
-
"Mend waits up to a minute for the port to answer before it returns. --wait
|
|
2666
|
+
"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.",
|
|
2239
2667
|
"The port is tunnelled to this machine's loopback as soon as it listens, unless --no-connect."
|
|
2240
2668
|
],
|
|
2241
2669
|
options: [
|
|
2242
2670
|
{
|
|
2243
2671
|
flag: "--wait",
|
|
2244
|
-
text: "
|
|
2672
|
+
text: "return once the port answers: 0; process ended 2, session gone 3; no tunnel"
|
|
2673
|
+
},
|
|
2674
|
+
{
|
|
2675
|
+
flag: "--timeout <duration>",
|
|
2676
|
+
text: "bound --wait (90, 90s, 5m, 1h); still starting then exits 124. Default: 10m"
|
|
2245
2677
|
},
|
|
2246
2678
|
{ flag: "--port <port>", text: "the port the command listens on inside the workspace" },
|
|
2247
2679
|
{ flag: "--name <n>", text: "the service's name. Default: the command" },
|
|
@@ -2251,9 +2683,13 @@ var COMMANDS = [
|
|
|
2251
2683
|
],
|
|
2252
2684
|
examples: [
|
|
2253
2685
|
{ command: "mend service run --port 3000 -- pnpm dev", text: "" },
|
|
2254
|
-
{ command: "mend service web", text: "the Service named web in mend.toml" }
|
|
2686
|
+
{ command: "mend service web", text: "the Service named web in mend.toml" },
|
|
2687
|
+
{
|
|
2688
|
+
command: "mend service run web --wait --timeout 15m",
|
|
2689
|
+
text: "returns once web answers; 124 if it is still starting after 15 minutes"
|
|
2690
|
+
}
|
|
2255
2691
|
],
|
|
2256
|
-
see: ["service init", "service connect", "service logs"]
|
|
2692
|
+
see: ["service init", "service connect", "service logs", "logs"]
|
|
2257
2693
|
},
|
|
2258
2694
|
{
|
|
2259
2695
|
name: "service add",
|
|
@@ -2286,9 +2722,13 @@ var COMMANDS = [
|
|
|
2286
2722
|
name: "service list",
|
|
2287
2723
|
section: "services",
|
|
2288
2724
|
summary: "every live service and its observed state",
|
|
2289
|
-
synopsis: [],
|
|
2290
|
-
description: [
|
|
2291
|
-
|
|
2725
|
+
synopsis: ["[--json]"],
|
|
2726
|
+
description: [
|
|
2727
|
+
"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.",
|
|
2728
|
+
"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)."
|
|
2729
|
+
],
|
|
2730
|
+
options: [{ flag: "--json", text: "print the Services as JSON" }],
|
|
2731
|
+
see: ["service run", "service connect", "logs"]
|
|
2292
2732
|
},
|
|
2293
2733
|
{
|
|
2294
2734
|
name: "service logs",
|
|
@@ -2807,13 +3247,14 @@ var COMMANDS = [
|
|
|
2807
3247
|
section: "this machine",
|
|
2808
3248
|
summary: "install or repair the local Mend server",
|
|
2809
3249
|
synopsis: [
|
|
2810
|
-
"[--context <name>] [--version <version|latest>] [--bind <ip>] [--ssh-bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--declare <item>...] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
|
|
3250
|
+
"[--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]"
|
|
2811
3251
|
],
|
|
2812
3252
|
description: [
|
|
2813
3253
|
"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.",
|
|
2814
3254
|
"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.",
|
|
2815
3255
|
"--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.",
|
|
2816
3256
|
"--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.",
|
|
3257
|
+
"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.",
|
|
2817
3258
|
"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.",
|
|
2818
3259
|
"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."
|
|
2819
3260
|
],
|
|
@@ -2858,6 +3299,35 @@ var COMMANDS = [
|
|
|
2858
3299
|
flag: "--declare <item>",
|
|
2859
3300
|
text: "state a gate item you verified from outside: core-private, edge-tls or workspace-ssh; repeat for more, none clears. Kept across reruns. public with --ssh-bind beyond loopback needs workspace-ssh"
|
|
2860
3301
|
},
|
|
3302
|
+
{
|
|
3303
|
+
flag: "--npm-mirror, --no-npm-mirror",
|
|
3304
|
+
text: "run or stop the npm mirror; kept across reruns and upgrades. Default: on"
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
flag: "--npm-mirror-max-size <size>",
|
|
3308
|
+
text: "the npm mirror's disk cap, such as 20g or 1536m, at least 1g. Default: 10g"
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
flag: "--docker-mirror, --no-docker-mirror",
|
|
3312
|
+
text: "run or stop the Docker Hub mirror; kept across reruns and upgrades. Default: on"
|
|
3313
|
+
},
|
|
3314
|
+
{
|
|
3315
|
+
flag: "--docker-mirror-max-size <size>",
|
|
3316
|
+
text: "the Docker mirror's cap, such as 40g; over it the cache is cleared and fills again. Default: 20g"
|
|
3317
|
+
},
|
|
3318
|
+
{
|
|
3319
|
+
flag: "--docker-hub-username <name>",
|
|
3320
|
+
text: "the Docker mirror pulls as this Docker Hub account; needs --docker-hub-token-stdin"
|
|
3321
|
+
},
|
|
3322
|
+
{
|
|
3323
|
+
flag: "--docker-hub-token-stdin",
|
|
3324
|
+
text: "read that account's access token from standard input; kept in server.env, never in argv or a session"
|
|
3325
|
+
},
|
|
3326
|
+
{
|
|
3327
|
+
flag: "--docker-hub-public-only",
|
|
3328
|
+
text: "required with a login: you state the token is scoped Public Repo Read-only, since every session can pull what it can read"
|
|
3329
|
+
},
|
|
3330
|
+
{ flag: "--no-docker-hub-login", text: "the Docker mirror pulls anonymously again" },
|
|
2861
3331
|
{
|
|
2862
3332
|
flag: "--assets-dir <dir>",
|
|
2863
3333
|
text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
|
|
@@ -2889,6 +3359,10 @@ var COMMANDS = [
|
|
|
2889
3359
|
{
|
|
2890
3360
|
command: "mend server setup --exposure public --tenancy multi",
|
|
2891
3361
|
text: "on an install with an edge and a first account: declare public, many organizations"
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
command: 'printf %s "$DOCKER_HUB_TOKEN" | mend server setup --docker-hub-username mendbot --docker-hub-token-stdin --docker-hub-public-only',
|
|
3365
|
+
text: "the Docker mirror pulls from Docker Hub as mendbot, with a Public Repo Read-only token"
|
|
2892
3366
|
}
|
|
2893
3367
|
],
|
|
2894
3368
|
see: ["server", "server status", "login", "doctor", "operator exposure"]
|
|
@@ -2896,10 +3370,11 @@ var COMMANDS = [
|
|
|
2896
3370
|
{
|
|
2897
3371
|
name: "server status",
|
|
2898
3372
|
section: "this machine",
|
|
2899
|
-
summary: "show the pin, generation, containers, edge and posture",
|
|
3373
|
+
summary: "show the pin, generation, containers, edge, mirrors and posture",
|
|
2900
3374
|
synopsis: [""],
|
|
2901
3375
|
description: [
|
|
2902
3376
|
"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.",
|
|
3377
|
+
"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.",
|
|
2903
3378
|
"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."
|
|
2904
3379
|
],
|
|
2905
3380
|
see: ["server logs", "server start", "operator gate", "operator exposure"]
|
|
@@ -2918,7 +3393,7 @@ var COMMANDS = [
|
|
|
2918
3393
|
{
|
|
2919
3394
|
name: "server stop",
|
|
2920
3395
|
section: "this machine",
|
|
2921
|
-
summary: "stop
|
|
3396
|
+
summary: "stop the server's containers without deleting data",
|
|
2922
3397
|
synopsis: [""],
|
|
2923
3398
|
description: [
|
|
2924
3399
|
"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."
|
|
@@ -2998,7 +3473,7 @@ var COMMANDS = [
|
|
|
2998
3473
|
summary: "workspace SSH status: gateway, registered keys, ssh config",
|
|
2999
3474
|
synopsis: ["[status]"],
|
|
3000
3475
|
description: ["Whether this machine can ssh into workspaces, and what is missing if not."],
|
|
3001
|
-
see: ["ssh setup"]
|
|
3476
|
+
see: ["ssh setup", "ssh keys"]
|
|
3002
3477
|
},
|
|
3003
3478
|
{
|
|
3004
3479
|
name: "ssh setup",
|
|
@@ -3012,7 +3487,35 @@ var COMMANDS = [
|
|
|
3012
3487
|
{ flag: "--key <path>", text: "the public key to register" },
|
|
3013
3488
|
{ flag: "--host <hostname>", text: "override only the SSH hostname" }
|
|
3014
3489
|
],
|
|
3015
|
-
see: ["ssh", "shell"]
|
|
3490
|
+
see: ["ssh", "ssh keys", "shell"]
|
|
3491
|
+
},
|
|
3492
|
+
{
|
|
3493
|
+
name: "ssh keys",
|
|
3494
|
+
section: "this machine",
|
|
3495
|
+
summary: "your registered workspace ssh keys, from every machine",
|
|
3496
|
+
synopsis: ["[--json]"],
|
|
3497
|
+
description: [
|
|
3498
|
+
"Every key your account registered with the workspace SSH gateway: fingerprint, name, algorithm and the day it was registered. The key this machine would offer is marked. The platform does not record when a key was last used."
|
|
3499
|
+
],
|
|
3500
|
+
options: [{ flag: "--json", text: "the keys as JSON, with thisMachine on each" }],
|
|
3501
|
+
see: ["ssh keys remove", "ssh setup"]
|
|
3502
|
+
},
|
|
3503
|
+
{
|
|
3504
|
+
name: "ssh keys remove",
|
|
3505
|
+
section: "this machine",
|
|
3506
|
+
summary: "stop the workspace ssh gateway accepting one of your keys",
|
|
3507
|
+
synopsis: ["<fingerprint>"],
|
|
3508
|
+
description: [
|
|
3509
|
+
"Removes one of your keys by fingerprint; the SHA256: prefix is optional. The gateway looks a key up on every new connection, so the next connection offering it is refused. A connection already open stays open until it ends.",
|
|
3510
|
+
"Only your own keys are listed and removed. The key file and the ~/.ssh/config block on the machine that registered it stay; mend ssh setup there registers it again. Removing a member from the organization removes all of theirs, and Mend keeps retrying any the platform refused."
|
|
3511
|
+
],
|
|
3512
|
+
examples: [
|
|
3513
|
+
{
|
|
3514
|
+
command: "mend ssh keys remove SHA256:Vn6v0P2dHq1n2a5aGQ6L7rKk8sWm0u3x1zYbTq9cE4o",
|
|
3515
|
+
text: "revoke a lost laptop's key"
|
|
3516
|
+
}
|
|
3517
|
+
],
|
|
3518
|
+
see: ["ssh keys"]
|
|
3016
3519
|
},
|
|
3017
3520
|
{
|
|
3018
3521
|
name: "accounts",
|
|
@@ -3038,7 +3541,7 @@ var COMMANDS = [
|
|
|
3038
3541
|
summary: "remove the server, this machine's Mend files, or both",
|
|
3039
3542
|
synopsis: ["[--all | --server | --home] [--yes]"],
|
|
3040
3543
|
description: [
|
|
3041
|
-
"Asks which scope to remove when none is given, prints exactly what will go, and requires the word delete before the server is touched. The server scope removes the local Compose installation: its containers, every volume it owns (repositories, worktrees, the database), its release image, and the private configuration with its generations and backups. The home scope
|
|
3544
|
+
"Asks which scope to remove when none is given, prints exactly what will go, and requires the word delete before the server is touched. The server scope removes the local Compose installation: its containers, every volume it owns (repositories, worktrees, the database), its release image, and the private configuration with its generations and backups. The home scope asks the server to remove the workspace SSH key this machine registered and to revoke this terminal's device token, then removes cli.json, the workspace SSH key file, and the managed block in ~/.ssh/config. The key is found by its public half; a key the server refuses to remove, or one this machine cannot read, makes the uninstall exit 1 naming what may still be registered. The account's other keys and devices stay.",
|
|
3042
3545
|
"Nothing else under the configuration directory is touched; a host-run store or keys root is listed and left in place. Workspace containers carry no label Mend can filter on, so they are named with the command that removes them, never removed."
|
|
3043
3546
|
],
|
|
3044
3547
|
options: [
|
|
@@ -3447,7 +3950,7 @@ var writeBundle = (files, outPath, now) => {
|
|
|
3447
3950
|
entries: entries.map((entry) => ({ path: entry.path, bytes: entry.content.length }))
|
|
3448
3951
|
};
|
|
3449
3952
|
};
|
|
3450
|
-
var
|
|
3953
|
+
var formatBytes2 = (bytes) => {
|
|
3451
3954
|
if (bytes < 1024) return `${bytes} B`;
|
|
3452
3955
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
3453
3956
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -3456,8 +3959,8 @@ var NOTICE = "Contains logs and configuration; secrets are redacted, but read it
|
|
|
3456
3959
|
var bundleReport = (written) => {
|
|
3457
3960
|
const width = Math.max(...written.entries.map((entry) => entry.path.length), 0);
|
|
3458
3961
|
return [
|
|
3459
|
-
`${written.path} \xB7 ${
|
|
3460
|
-
...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${
|
|
3962
|
+
`${written.path} \xB7 ${formatBytes2(written.bytes)}`,
|
|
3963
|
+
...written.entries.map((entry) => ` ${entry.path.padEnd(width)} ${formatBytes2(entry.bytes)}`),
|
|
3461
3964
|
NOTICE
|
|
3462
3965
|
];
|
|
3463
3966
|
};
|
|
@@ -4602,7 +5105,7 @@ var fetchedLines = (fetched) => {
|
|
|
4602
5105
|
lines.push(`${dim(" switch to it")} git switch ${fetched.branch}`);
|
|
4603
5106
|
return lines;
|
|
4604
5107
|
};
|
|
4605
|
-
var
|
|
5108
|
+
var formatBytes3 = (bytes) => {
|
|
4606
5109
|
if (bytes < 1024) return `${bytes} bytes`;
|
|
4607
5110
|
const units = ["KiB", "MiB", "GiB"];
|
|
4608
5111
|
let value = bytes / 1024;
|
|
@@ -4635,7 +5138,7 @@ var bundleRefusal = (downloaded) => {
|
|
|
4635
5138
|
const size = numberField(body, "size");
|
|
4636
5139
|
const limit = numberField(body, "limit");
|
|
4637
5140
|
if (downloaded.status === 413 && size !== null && limit !== null) {
|
|
4638
|
-
return `bundle not sent \xB7 ${
|
|
5141
|
+
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`;
|
|
4639
5142
|
}
|
|
4640
5143
|
return stringField2(body, "message") ?? `the bundle request answered ${downloaded.status}`;
|
|
4641
5144
|
};
|
|
@@ -5030,7 +5533,10 @@ var modelCatalogLines = (catalogs, dim8 = (text) => text) => {
|
|
|
5030
5533
|
}
|
|
5031
5534
|
return lines;
|
|
5032
5535
|
};
|
|
5033
|
-
var modelCatalogJson = (catalogs) =>
|
|
5536
|
+
var modelCatalogJson = (catalogs) => ({
|
|
5537
|
+
version: 1,
|
|
5538
|
+
harnesses: catalogs
|
|
5539
|
+
});
|
|
5034
5540
|
|
|
5035
5541
|
// src/organization.ts
|
|
5036
5542
|
import * as fs12 from "node:fs";
|
|
@@ -5667,21 +6173,39 @@ var piProfileLines = (scan) => {
|
|
|
5667
6173
|
|
|
5668
6174
|
// src/run-scripts.ts
|
|
5669
6175
|
var RUN_ARGV_MAX = 64;
|
|
6176
|
+
var RUN_ARGV_MAX_WORD_BYTES = 128 * 1024 - 1;
|
|
6177
|
+
var RUN_ARGV_MAX_TOTAL_BYTES = 1024 * 1024;
|
|
5670
6178
|
var whitespaceName = (character) => character === "\n" || character === "\r" ? "a newline" : character === " " ? "a tab" : character === " " ? "a space" : "whitespace";
|
|
6179
|
+
var LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
6180
|
+
var utf8Bytes = (value) => Buffer.byteLength(value, "utf8");
|
|
5671
6181
|
var runArgvIssue = (argv) => {
|
|
5672
6182
|
if (argv.length > RUN_ARGV_MAX) {
|
|
5673
6183
|
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`;
|
|
5674
6184
|
}
|
|
6185
|
+
const program = argv[0] ?? "";
|
|
6186
|
+
if (program === "") return "the program is empty";
|
|
6187
|
+
if (program.trim() !== program) {
|
|
6188
|
+
const leading = program.trimStart() !== program;
|
|
6189
|
+
const edge = leading ? program.charAt(0) : program.charAt(program.length - 1);
|
|
6190
|
+
return `the program ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 trim it and run again`;
|
|
6191
|
+
}
|
|
6192
|
+
let total = 0;
|
|
5675
6193
|
for (const [index, word] of argv.entries()) {
|
|
5676
6194
|
const which = index === 0 ? "the program" : `argument ${index}`;
|
|
5677
|
-
if (word
|
|
5678
|
-
return `${which}
|
|
6195
|
+
if (word.includes("\0")) {
|
|
6196
|
+
return `${which} contains a NUL byte, which no process argument can carry`;
|
|
6197
|
+
}
|
|
6198
|
+
if (LONE_SURROGATE.test(word)) {
|
|
6199
|
+
return `${which} is not well-formed Unicode (a lone surrogate)`;
|
|
5679
6200
|
}
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
return `${which} ${leading ? "starts" : "ends"} with ${whitespaceName(edge)} \xB7 the platform refuses arguments with leading or trailing whitespace \xB7 trim it and run again`;
|
|
6201
|
+
const bytes = utf8Bytes(word);
|
|
6202
|
+
if (bytes > RUN_ARGV_MAX_WORD_BYTES) {
|
|
6203
|
+
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`;
|
|
5684
6204
|
}
|
|
6205
|
+
total += bytes;
|
|
6206
|
+
}
|
|
6207
|
+
if (total > RUN_ARGV_MAX_TOTAL_BYTES) {
|
|
6208
|
+
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`;
|
|
5685
6209
|
}
|
|
5686
6210
|
return null;
|
|
5687
6211
|
};
|
|
@@ -5834,16 +6358,22 @@ var splitArgs = (args, valued, switches) => {
|
|
|
5834
6358
|
return { values, on, session };
|
|
5835
6359
|
};
|
|
5836
6360
|
var parseLogsArgs = (args) => {
|
|
5837
|
-
const split = splitArgs(args, ["--from", "--process"], ["--follow", "-f"]);
|
|
6361
|
+
const split = splitArgs(args, ["--from", "--process", "--service"], ["--follow", "-f"]);
|
|
5838
6362
|
if ("error" in split) return split;
|
|
5839
6363
|
const from = split.values.get("--from") ?? "0";
|
|
5840
6364
|
if (!/^(0|[1-9]\d*)$/u.test(from)) return { error: "--from takes a record sequence, e.g. 0" };
|
|
6365
|
+
const process2 = split.values.get("--process") ?? null;
|
|
6366
|
+
const service = split.values.get("--service") ?? null;
|
|
6367
|
+
if (process2 !== null && service !== null) {
|
|
6368
|
+
return { error: "--process and --service each name one process; pass one of them" };
|
|
6369
|
+
}
|
|
5841
6370
|
return {
|
|
5842
6371
|
args: {
|
|
5843
6372
|
session: split.session,
|
|
5844
6373
|
follow: split.on.has("--follow") || split.on.has("-f"),
|
|
5845
6374
|
from,
|
|
5846
|
-
process:
|
|
6375
|
+
process: process2,
|
|
6376
|
+
service
|
|
5847
6377
|
}
|
|
5848
6378
|
};
|
|
5849
6379
|
};
|
|
@@ -5851,21 +6381,149 @@ var parseWaitArgs = (args) => {
|
|
|
5851
6381
|
const split = splitArgs(args, ["--timeout", "--process"], ["--json"]);
|
|
5852
6382
|
if ("error" in split) return split;
|
|
5853
6383
|
const timeout = split.values.get("--timeout");
|
|
5854
|
-
const
|
|
5855
|
-
if (
|
|
5856
|
-
return { error: "--timeout takes a number of seconds above 0" };
|
|
5857
|
-
}
|
|
6384
|
+
const timeoutMs = timeout === void 0 ? null : parseDuration(timeout);
|
|
6385
|
+
if (timeoutMs === null && timeout !== void 0) return { error: DURATION_ISSUE };
|
|
5858
6386
|
return {
|
|
5859
6387
|
args: {
|
|
5860
6388
|
session: split.session,
|
|
5861
6389
|
process: split.values.get("--process") ?? null,
|
|
5862
|
-
timeoutMs
|
|
6390
|
+
timeoutMs,
|
|
5863
6391
|
json: split.on.has("--json")
|
|
5864
6392
|
}
|
|
5865
6393
|
};
|
|
5866
6394
|
};
|
|
6395
|
+
var DURATION_UNIT_MS = { s: 1e3, m: 6e4, h: 36e5 };
|
|
6396
|
+
var parseDuration = (text) => {
|
|
6397
|
+
const trimmed = text.trim();
|
|
6398
|
+
const unit = DURATION_UNIT_MS[trimmed.slice(-1)];
|
|
6399
|
+
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;
|
|
6400
|
+
const ms = Math.ceil(amount * (unit ?? 1e3));
|
|
6401
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
6402
|
+
};
|
|
6403
|
+
var DURATION_ISSUE = "--timeout takes a duration above 0: 90 or 90s, 5m, 1h";
|
|
6404
|
+
var durationLine = (ms) => {
|
|
6405
|
+
const seconds2 = Math.round(ms / 1e3);
|
|
6406
|
+
if (seconds2 < 60) return `${seconds2} s`;
|
|
6407
|
+
const minutes = Math.floor(seconds2 / 60);
|
|
6408
|
+
const rest = seconds2 % 60;
|
|
6409
|
+
if (minutes < 60) return rest === 0 ? `${minutes} min` : `${minutes} min ${rest} s`;
|
|
6410
|
+
const hours = Math.floor(minutes / 60);
|
|
6411
|
+
return minutes % 60 === 0 ? `${hours} h` : `${hours} h ${minutes % 60} min`;
|
|
6412
|
+
};
|
|
5867
6413
|
var WAIT_TIMED_OUT = 124;
|
|
5868
6414
|
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);
|
|
6415
|
+
var SERVICE_PROCESS_ENDED = 2;
|
|
6416
|
+
var SERVICE_WORKSPACE_ENDED = 3;
|
|
6417
|
+
var begunBy = (view, attempt2, startId) => attempt2.launchCorrelationId === serviceStartCorrelation(view.service.id, startId);
|
|
6418
|
+
var findStartAttempt = (services, target) => {
|
|
6419
|
+
for (const view of services) {
|
|
6420
|
+
if (view.service.sessionId !== target.sessionId) continue;
|
|
6421
|
+
const attempt2 = view.attempts.find(
|
|
6422
|
+
(candidate) => target.attemptId !== null ? candidate.id === target.attemptId : begunBy(view, candidate, target.startId)
|
|
6423
|
+
);
|
|
6424
|
+
if (attempt2 !== void 0) return { view, attempt: attempt2 };
|
|
6425
|
+
}
|
|
6426
|
+
return void 0;
|
|
6427
|
+
};
|
|
6428
|
+
var STAMP_KIND = serviceStartCorrelation("", "").split(":")[0] ?? "service-start";
|
|
6429
|
+
var answeredAttemptId = (view, startId) => {
|
|
6430
|
+
const own = view.attempts.find((attempt2) => begunBy(view, attempt2, startId));
|
|
6431
|
+
if (own !== void 0) return own.id;
|
|
6432
|
+
const stamps = view.attempts.some(
|
|
6433
|
+
(attempt2) => attempt2.launchCorrelationId?.startsWith(`${STAMP_KIND}:`) === true
|
|
6434
|
+
);
|
|
6435
|
+
return stamps ? null : view.service.currentAttemptId;
|
|
6436
|
+
};
|
|
6437
|
+
var timeOf = (iso) => {
|
|
6438
|
+
if (iso === void 0) return null;
|
|
6439
|
+
const at = Date.parse(iso);
|
|
6440
|
+
return Number.isFinite(at) ? at : null;
|
|
6441
|
+
};
|
|
6442
|
+
var answeredFor = (view, attempt2) => {
|
|
6443
|
+
const observation = view.latestObservation;
|
|
6444
|
+
if (view.service.currentAttemptId !== attempt2.id || view.currentForward === null) return false;
|
|
6445
|
+
if (observation === null || observation.forwardId !== view.currentForward.id) return false;
|
|
6446
|
+
if (observation.state !== "reachable") return false;
|
|
6447
|
+
const began = timeOf(attempt2.createdAt);
|
|
6448
|
+
const observed = timeOf(observation.lastObservedAt);
|
|
6449
|
+
return began === null || observed === null || observed >= began;
|
|
6450
|
+
};
|
|
6451
|
+
var serviceStartStateOf = (read, target) => {
|
|
6452
|
+
const found = findStartAttempt(read.services, target);
|
|
6453
|
+
if (found !== void 0 && found.attempt.exitedAt !== null) {
|
|
6454
|
+
return {
|
|
6455
|
+
kind: "process-ended",
|
|
6456
|
+
processId: found.attempt.id,
|
|
6457
|
+
status: found.attempt.status,
|
|
6458
|
+
exitCode: found.attempt.exitCode ?? null
|
|
6459
|
+
};
|
|
6460
|
+
}
|
|
6461
|
+
if (read.sessionStatus === null) return { kind: "workspace-ended", sessionStatus: null };
|
|
6462
|
+
if (found === void 0) return { kind: "starting", processId: target.attemptId };
|
|
6463
|
+
return answeredFor(found.view, found.attempt) ? { kind: "answered", processId: found.attempt.id, view: found.view } : { kind: "starting", processId: found.attempt.id };
|
|
6464
|
+
};
|
|
6465
|
+
var waitForServiceStart = async (options) => {
|
|
6466
|
+
const pollMs = options.pollMs ?? 2e3;
|
|
6467
|
+
const discoverWithinMs = options.unreachableAfterMs ?? 6e4;
|
|
6468
|
+
const since = options.now();
|
|
6469
|
+
let target = options.target;
|
|
6470
|
+
let last = null;
|
|
6471
|
+
for (; ; ) {
|
|
6472
|
+
const got = await readThrough(options.read, options, options.deadline);
|
|
6473
|
+
if (!got.done) return { kind: "timeout", last };
|
|
6474
|
+
const state = serviceStartStateOf(got.value, target);
|
|
6475
|
+
if (state.kind !== "starting") return state;
|
|
6476
|
+
last = state;
|
|
6477
|
+
if (state.processId === null) {
|
|
6478
|
+
if (options.now() - since >= discoverWithinMs) return { kind: "no-attempt" };
|
|
6479
|
+
} else if (target.attemptId === null) {
|
|
6480
|
+
target = { ...target, attemptId: state.processId };
|
|
6481
|
+
}
|
|
6482
|
+
options.onStarting?.(state);
|
|
6483
|
+
const left = options.deadline === null ? pollMs : Math.min(pollMs, options.deadline - options.now());
|
|
6484
|
+
if (left <= 0) return { kind: "timeout", last };
|
|
6485
|
+
await options.sleep(left);
|
|
6486
|
+
}
|
|
6487
|
+
};
|
|
6488
|
+
var readWithin = (read, clock2, deadline) => readThrough(read, clock2, deadline);
|
|
6489
|
+
var candidateLine = (view) => `${view.service.id} (session ${view.service.sessionId.slice(0, 8)})`;
|
|
6490
|
+
var pickServiceAttempt = (services, needle, sessionId) => {
|
|
6491
|
+
const inScope = services.filter(
|
|
6492
|
+
(view) => sessionId === null || view.service.sessionId === sessionId
|
|
6493
|
+
);
|
|
6494
|
+
const byId = inScope.filter((view) => view.service.id === needle);
|
|
6495
|
+
const byName = inScope.filter((view) => view.service.name === needle);
|
|
6496
|
+
const matches = byId.length > 0 ? byId : byName.length > 0 ? byName : inScope.filter((view) => view.service.id.startsWith(needle));
|
|
6497
|
+
const [chosen] = matches;
|
|
6498
|
+
if (chosen === void 0) {
|
|
6499
|
+
return {
|
|
6500
|
+
error: `no Service ${sessionId === null ? "" : "of this session "}is named "${needle}" or has an id starting with it`,
|
|
6501
|
+
named: false
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
if (matches.length > 1) {
|
|
6505
|
+
return {
|
|
6506
|
+
error: `"${needle}" names ${matches.length} Services \xB7 name one by its id: ${matches.map(candidateLine).join(", ")}`,
|
|
6507
|
+
named: true
|
|
6508
|
+
};
|
|
6509
|
+
}
|
|
6510
|
+
const name = chosen.service.name;
|
|
6511
|
+
const attemptId = chosen.service.currentAttemptId;
|
|
6512
|
+
if (attemptId === null) {
|
|
6513
|
+
return {
|
|
6514
|
+
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)`,
|
|
6515
|
+
named: true
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6518
|
+
const attempt2 = chosen.attempts.find((candidate) => candidate.id === attemptId);
|
|
6519
|
+
if (attempt2 === void 0 || attempt2.sealantSessionId === null) {
|
|
6520
|
+
return {
|
|
6521
|
+
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`,
|
|
6522
|
+
named: true
|
|
6523
|
+
};
|
|
6524
|
+
}
|
|
6525
|
+
return { service: chosen, processId: attemptId };
|
|
6526
|
+
};
|
|
5869
6527
|
|
|
5870
6528
|
// src/secret-files.ts
|
|
5871
6529
|
import * as path14 from "node:path";
|
|
@@ -6171,7 +6829,14 @@ var writeDurable = (file, content, mode = 384) => {
|
|
|
6171
6829
|
}
|
|
6172
6830
|
};
|
|
6173
6831
|
var fileKeys = ["identity", "config", "env", "compose", "postgresInit"];
|
|
6174
|
-
var optionalFileKeys = [
|
|
6832
|
+
var optionalFileKeys = [
|
|
6833
|
+
"posture",
|
|
6834
|
+
"edge",
|
|
6835
|
+
"caddyfile",
|
|
6836
|
+
"mirrors",
|
|
6837
|
+
"npmMirrorConf",
|
|
6838
|
+
"dockerMirrorGuard"
|
|
6839
|
+
];
|
|
6175
6840
|
var fileNames = {
|
|
6176
6841
|
identity: "identity.env",
|
|
6177
6842
|
config: "server.json",
|
|
@@ -6180,7 +6845,10 @@ var fileNames = {
|
|
|
6180
6845
|
postgresInit: "postgres-init.sh",
|
|
6181
6846
|
posture: "compose.posture.yaml",
|
|
6182
6847
|
edge: "compose.edge.yaml",
|
|
6183
|
-
caddyfile: "Caddyfile"
|
|
6848
|
+
caddyfile: "Caddyfile",
|
|
6849
|
+
mirrors: "compose.mirrors.yaml",
|
|
6850
|
+
npmMirrorConf: "npm-mirror.conf",
|
|
6851
|
+
dockerMirrorGuard: "docker-mirror-guard.sh"
|
|
6184
6852
|
};
|
|
6185
6853
|
var fileModes = {
|
|
6186
6854
|
identity: 384,
|
|
@@ -6190,7 +6858,10 @@ var fileModes = {
|
|
|
6190
6858
|
postgresInit: 493,
|
|
6191
6859
|
posture: 384,
|
|
6192
6860
|
edge: 384,
|
|
6193
|
-
caddyfile: 420
|
|
6861
|
+
caddyfile: 420,
|
|
6862
|
+
mirrors: 384,
|
|
6863
|
+
npmMirrorConf: 420,
|
|
6864
|
+
dockerMirrorGuard: 420
|
|
6194
6865
|
};
|
|
6195
6866
|
var allFiles = (files) => [...fileKeys, ...optionalFileKeys].map((key) => [key, files[key]]);
|
|
6196
6867
|
var parseLockOwner = (raw) => {
|
|
@@ -6294,6 +6965,9 @@ var readActive = (paths) => {
|
|
|
6294
6965
|
const posture = optional("posture");
|
|
6295
6966
|
const edge = optional("edge");
|
|
6296
6967
|
const caddyfile = optional("caddyfile");
|
|
6968
|
+
const mirrors = optional("mirrors");
|
|
6969
|
+
const npmMirrorConf = optional("npmMirrorConf");
|
|
6970
|
+
const dockerMirrorGuard = optional("dockerMirrorGuard");
|
|
6297
6971
|
const files = {
|
|
6298
6972
|
identity: fs14.readFileSync(path15.join(directory, fileNames.identity), "utf8"),
|
|
6299
6973
|
config: fs14.readFileSync(path15.join(directory, fileNames.config), "utf8"),
|
|
@@ -6302,7 +6976,10 @@ var readActive = (paths) => {
|
|
|
6302
6976
|
postgresInit: fs14.readFileSync(path15.join(directory, fileNames.postgresInit), "utf8"),
|
|
6303
6977
|
...posture === void 0 ? {} : { posture },
|
|
6304
6978
|
...edge === void 0 ? {} : { edge },
|
|
6305
|
-
...caddyfile === void 0 ? {} : { caddyfile }
|
|
6979
|
+
...caddyfile === void 0 ? {} : { caddyfile },
|
|
6980
|
+
...mirrors === void 0 ? {} : { mirrors },
|
|
6981
|
+
...npmMirrorConf === void 0 ? {} : { npmMirrorConf },
|
|
6982
|
+
...dockerMirrorGuard === void 0 ? {} : { dockerMirrorGuard }
|
|
6306
6983
|
};
|
|
6307
6984
|
if (readIdentity(paths) !== files.identity) {
|
|
6308
6985
|
throw new ServerStoreError(
|
|
@@ -6852,7 +7529,28 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6852
7529
|
"--no-edge",
|
|
6853
7530
|
"--exposure",
|
|
6854
7531
|
"--tenancy",
|
|
6855
|
-
"--declare"
|
|
7532
|
+
"--declare",
|
|
7533
|
+
"--npm-mirror",
|
|
7534
|
+
"--no-npm-mirror",
|
|
7535
|
+
"--npm-mirror-max-size",
|
|
7536
|
+
"--docker-mirror",
|
|
7537
|
+
"--no-docker-mirror",
|
|
7538
|
+
"--docker-mirror-max-size",
|
|
7539
|
+
"--docker-hub-username",
|
|
7540
|
+
"--docker-hub-token-stdin",
|
|
7541
|
+
"--docker-hub-public-only",
|
|
7542
|
+
"--no-docker-hub-login"
|
|
7543
|
+
]);
|
|
7544
|
+
var SWITCHES = /* @__PURE__ */ new Set([
|
|
7545
|
+
"--offline",
|
|
7546
|
+
"--no-edge",
|
|
7547
|
+
"--npm-mirror",
|
|
7548
|
+
"--no-npm-mirror",
|
|
7549
|
+
"--docker-mirror",
|
|
7550
|
+
"--no-docker-mirror",
|
|
7551
|
+
"--docker-hub-token-stdin",
|
|
7552
|
+
"--docker-hub-public-only",
|
|
7553
|
+
"--no-docker-hub-login"
|
|
6856
7554
|
]);
|
|
6857
7555
|
var parseExposure = (value) => {
|
|
6858
7556
|
if (!isExposure(value))
|
|
@@ -6895,7 +7593,7 @@ var parseSetupOptions = (args) => {
|
|
|
6895
7593
|
if (flag === void 0) continue;
|
|
6896
7594
|
if (!flag.startsWith("--")) throw setupError(`Unexpected server setup argument "${flag}".`);
|
|
6897
7595
|
if (!SETUP_FLAGS.has(flag)) throw setupError(`Unknown server setup option "${flag}".`);
|
|
6898
|
-
if (flag
|
|
7596
|
+
if (SWITCHES.has(flag)) {
|
|
6899
7597
|
if (values.has(flag)) throw setupError(`${flag} may be supplied only once.`);
|
|
6900
7598
|
values.set(flag, ["true"]);
|
|
6901
7599
|
continue;
|
|
@@ -6920,6 +7618,51 @@ var parseSetupOptions = (args) => {
|
|
|
6920
7618
|
const declared = values.get("--declare");
|
|
6921
7619
|
if (edge !== void 0 && values.has("--no-edge"))
|
|
6922
7620
|
throw setupError("--edge and --no-edge contradict each other.");
|
|
7621
|
+
const pair = (on, off) => {
|
|
7622
|
+
if (values.has(on) && values.has(off))
|
|
7623
|
+
throw setupError(`${on} and ${off} contradict each other.`);
|
|
7624
|
+
return values.has(on) ? true : values.has(off) ? false : void 0;
|
|
7625
|
+
};
|
|
7626
|
+
const npmMirror = pair("--npm-mirror", "--no-npm-mirror");
|
|
7627
|
+
const dockerMirror = pair("--docker-mirror", "--no-docker-mirror");
|
|
7628
|
+
const maxSize = flagValue3("--npm-mirror-max-size");
|
|
7629
|
+
const npmMirrorMaxSize = maxSize === void 0 ? void 0 : parseMirrorSize(maxSize);
|
|
7630
|
+
if (npmMirrorMaxSize === null)
|
|
7631
|
+
throw setupError(
|
|
7632
|
+
`--npm-mirror-max-size must be a whole number of gibibytes or mebibytes, at least 1g, such as 20g or 1536m, not "${maxSize ?? ""}".`
|
|
7633
|
+
);
|
|
7634
|
+
if (npmMirrorMaxSize !== void 0 && npmMirror === false)
|
|
7635
|
+
throw setupError("--npm-mirror-max-size and --no-npm-mirror contradict each other.");
|
|
7636
|
+
const dockerMaxSize = flagValue3("--docker-mirror-max-size");
|
|
7637
|
+
const dockerMirrorMaxSize = dockerMaxSize === void 0 ? void 0 : parseMirrorSize(dockerMaxSize);
|
|
7638
|
+
if (dockerMirrorMaxSize === null)
|
|
7639
|
+
throw setupError(
|
|
7640
|
+
`--docker-mirror-max-size must be a whole number of gibibytes or mebibytes, at least 1g, such as 40g, not "${dockerMaxSize ?? ""}".`
|
|
7641
|
+
);
|
|
7642
|
+
if (dockerMirrorMaxSize !== void 0 && dockerMirror === false)
|
|
7643
|
+
throw setupError("--docker-mirror-max-size and --no-docker-mirror contradict each other.");
|
|
7644
|
+
const dockerHubUsername = flagValue3("--docker-hub-username");
|
|
7645
|
+
const dockerHubTokenStdin = values.has("--docker-hub-token-stdin");
|
|
7646
|
+
const noDockerHubLogin = values.has("--no-docker-hub-login");
|
|
7647
|
+
if (dockerHubUsername === void 0 !== !dockerHubTokenStdin)
|
|
7648
|
+
throw setupError(
|
|
7649
|
+
"--docker-hub-username and --docker-hub-token-stdin go together: the user name, and the access token on standard input."
|
|
7650
|
+
);
|
|
7651
|
+
if (dockerHubUsername !== void 0 && !isDockerHubCredential(dockerHubUsername))
|
|
7652
|
+
throw setupError(`--docker-hub-username "${dockerHubUsername}" is not a Docker Hub user name.`);
|
|
7653
|
+
const dockerHubPublicOnly = values.has("--docker-hub-public-only");
|
|
7654
|
+
if (dockerHubPublicOnly && dockerHubUsername === void 0)
|
|
7655
|
+
throw setupError(
|
|
7656
|
+
"--docker-hub-public-only goes with --docker-hub-username and --docker-hub-token-stdin: it states what the token you give can read."
|
|
7657
|
+
);
|
|
7658
|
+
if (dockerHubUsername !== void 0 && !dockerHubPublicOnly)
|
|
7659
|
+
throw setupError(
|
|
7660
|
+
`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.`
|
|
7661
|
+
);
|
|
7662
|
+
if (dockerHubUsername !== void 0 && (noDockerHubLogin || dockerMirror === false))
|
|
7663
|
+
throw setupError(
|
|
7664
|
+
`--docker-hub-username and ${noDockerHubLogin ? "--no-docker-hub-login" : "--no-docker-mirror"} contradict each other.`
|
|
7665
|
+
);
|
|
6923
7666
|
return {
|
|
6924
7667
|
context: flagValue3("--context"),
|
|
6925
7668
|
version: version === void 0 ? void 0 : parseVersion(version),
|
|
@@ -6936,9 +7679,60 @@ var parseSetupOptions = (args) => {
|
|
|
6936
7679
|
noEdge: values.has("--no-edge"),
|
|
6937
7680
|
exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
|
|
6938
7681
|
tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy),
|
|
6939
|
-
declared: declared === void 0 ? void 0 : parseDeclared(declared)
|
|
7682
|
+
declared: declared === void 0 ? void 0 : parseDeclared(declared),
|
|
7683
|
+
npmMirror,
|
|
7684
|
+
npmMirrorMaxSize,
|
|
7685
|
+
dockerMirror,
|
|
7686
|
+
dockerMirrorMaxSize,
|
|
7687
|
+
dockerHubUsername,
|
|
7688
|
+
dockerHubTokenStdin,
|
|
7689
|
+
noDockerHubLogin
|
|
7690
|
+
};
|
|
7691
|
+
};
|
|
7692
|
+
var resolveMirrors = (saved, options) => {
|
|
7693
|
+
const base = saved ?? DEFAULT_MIRRORS;
|
|
7694
|
+
const npmOn = options.npmMirror ?? (options.npmMirrorMaxSize !== void 0 || base.npm !== null);
|
|
7695
|
+
const dockerOn = options.dockerMirror ?? (options.dockerHubUsername !== void 0 || options.dockerMirrorMaxSize !== void 0 || base.docker !== null);
|
|
7696
|
+
const upstreamUser = options.dockerHubUsername ?? (options.noDockerHubLogin ? void 0 : dockerMirrorLogin(base.docker));
|
|
7697
|
+
const dockerMaxSize = options.dockerMirrorMaxSize ?? base.docker?.maxSize ?? DEFAULT_DOCKER_MIRROR_MAX_SIZE;
|
|
7698
|
+
return {
|
|
7699
|
+
npm: npmOn ? {
|
|
7700
|
+
maxSize: options.npmMirrorMaxSize ?? base.npm?.maxSize ?? DEFAULT_NPM_MIRROR_MAX_SIZE
|
|
7701
|
+
} : null,
|
|
7702
|
+
docker: !dockerOn ? null : upstreamUser === void 0 ? { maxSize: dockerMaxSize } : { maxSize: dockerMaxSize, upstreamUser, upstreamPublicOnly: true }
|
|
6940
7703
|
};
|
|
6941
7704
|
};
|
|
7705
|
+
var parseMirrorsField = (value) => {
|
|
7706
|
+
const fields = ownFields(value);
|
|
7707
|
+
const corrupt = () => setupError(
|
|
7708
|
+
"Server config is corrupt: mirrors must hold npm and docker, each an object or null."
|
|
7709
|
+
);
|
|
7710
|
+
if (fields === null || !fields.has("npm") || !fields.has("docker")) throw corrupt();
|
|
7711
|
+
const npm = fields.get("npm");
|
|
7712
|
+
const docker2 = fields.get("docker");
|
|
7713
|
+
let parsedNpm = null;
|
|
7714
|
+
if (npm !== null) {
|
|
7715
|
+
const maxSize = ownFields(npm)?.get("maxSize");
|
|
7716
|
+
if (typeof maxSize !== "string" || parseMirrorSize(maxSize) !== maxSize) throw corrupt();
|
|
7717
|
+
parsedNpm = { maxSize };
|
|
7718
|
+
}
|
|
7719
|
+
let parsedDocker = null;
|
|
7720
|
+
if (docker2 !== null) {
|
|
7721
|
+
const dockerFields = ownFields(docker2);
|
|
7722
|
+
if (dockerFields === null) throw corrupt();
|
|
7723
|
+
const maxSize = dockerFields.get("maxSize");
|
|
7724
|
+
if (typeof maxSize !== "string" || parseMirrorSize(maxSize) !== maxSize) throw corrupt();
|
|
7725
|
+
const user = dockerFields.get("upstreamUser");
|
|
7726
|
+
if (user !== void 0 && (typeof user !== "string" || !isDockerHubCredential(user)))
|
|
7727
|
+
throw corrupt();
|
|
7728
|
+
if (user !== void 0 && dockerFields.get("upstreamPublicOnly") !== true)
|
|
7729
|
+
throw setupError(
|
|
7730
|
+
"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."
|
|
7731
|
+
);
|
|
7732
|
+
parsedDocker = user === void 0 ? { maxSize } : { maxSize, upstreamUser: user, upstreamPublicOnly: true };
|
|
7733
|
+
}
|
|
7734
|
+
return { npm: parsedNpm, docker: parsedDocker };
|
|
7735
|
+
};
|
|
6942
7736
|
var parseUrl = (input, label) => {
|
|
6943
7737
|
try {
|
|
6944
7738
|
return new URL(input);
|
|
@@ -7137,7 +7931,8 @@ var parseServerConfig = (raw) => {
|
|
|
7137
7931
|
...edgeHost === void 0 ? {} : { edgeHost },
|
|
7138
7932
|
...exposure === void 0 ? {} : { exposure },
|
|
7139
7933
|
...tenancy === void 0 ? {} : { tenancy },
|
|
7140
|
-
...declared.length === 0 ? {} : { declared }
|
|
7934
|
+
...declared.length === 0 ? {} : { declared },
|
|
7935
|
+
...fields.has("mirrors") ? { mirrors: parseMirrorsField(fields.get("mirrors")) } : {}
|
|
7141
7936
|
};
|
|
7142
7937
|
if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
|
|
7143
7938
|
throw setupError(
|
|
@@ -7164,7 +7959,14 @@ var parseServerConfig = (raw) => {
|
|
|
7164
7959
|
noEdge: false,
|
|
7165
7960
|
exposure: config.exposure,
|
|
7166
7961
|
tenancy: config.tenancy,
|
|
7167
|
-
declared: config.declared
|
|
7962
|
+
declared: config.declared,
|
|
7963
|
+
npmMirror: void 0,
|
|
7964
|
+
npmMirrorMaxSize: void 0,
|
|
7965
|
+
dockerMirror: void 0,
|
|
7966
|
+
dockerMirrorMaxSize: void 0,
|
|
7967
|
+
dockerHubUsername: void 0,
|
|
7968
|
+
dockerHubTokenStdin: false,
|
|
7969
|
+
noDockerHubLogin: false
|
|
7168
7970
|
});
|
|
7169
7971
|
return config;
|
|
7170
7972
|
};
|
|
@@ -7195,8 +7997,12 @@ var parseSecrets = (raw) => {
|
|
|
7195
7997
|
betterAuthSecret: envValue(fields, "BETTER_AUTH_SECRET"),
|
|
7196
7998
|
sealantCredentialsKey: envValue(fields, "SEALANT_CREDENTIALS_KEY"),
|
|
7197
7999
|
sealantServiceKey: envValue(fields, "SEALANT_SERVICE_KEY"),
|
|
7198
|
-
workspaceSshGatewayToken: envValue(fields, "WORKSPACE_SSH_GATEWAY_TOKEN")
|
|
8000
|
+
workspaceSshGatewayToken: envValue(fields, "WORKSPACE_SSH_GATEWAY_TOKEN"),
|
|
8001
|
+
...fields.has("MEND_DOCKER_HUB_TOKEN") ? { dockerHubToken: envValue(fields, "MEND_DOCKER_HUB_TOKEN") } : {}
|
|
7199
8002
|
};
|
|
8003
|
+
if (secrets.dockerHubToken !== void 0 && !isDockerHubCredential(secrets.dockerHubToken)) {
|
|
8004
|
+
throw setupError("Server secrets are corrupt: MEND_DOCKER_HUB_TOKEN has an invalid value.");
|
|
8005
|
+
}
|
|
7200
8006
|
if (!/^slt_svc_[0-9a-f]{64}$/.test(secrets.sealantServiceKey)) {
|
|
7201
8007
|
throw setupError("Server secrets are corrupt: SEALANT_SERVICE_KEY has an invalid value.");
|
|
7202
8008
|
}
|
|
@@ -7523,6 +8329,9 @@ var renderSecrets = (secrets, config) => {
|
|
|
7523
8329
|
"MEND_STORE_VOLUME_NAME=mend-store",
|
|
7524
8330
|
"MEND_CONTROL_VOLUME_NAME=mend-control",
|
|
7525
8331
|
...renderGarage(secrets, config),
|
|
8332
|
+
// The mirrors' size cap and Docker Hub login (server-mirrors.ts); the token reaches only the
|
|
8333
|
+
// Docker mirror's container.
|
|
8334
|
+
...mirrorsEnvLines(config.mirrors, secrets.dockerHubToken),
|
|
7526
8335
|
`DOCKER_SOCKET_PATH=${config.dockerSocket}`,
|
|
7527
8336
|
""
|
|
7528
8337
|
].join("\n");
|
|
@@ -7573,6 +8382,47 @@ var validateOverlays = (config, files) => {
|
|
|
7573
8382
|
}
|
|
7574
8383
|
return edgeImageOf(files.edge);
|
|
7575
8384
|
};
|
|
8385
|
+
var validateMirrors = (config, secrets, files) => {
|
|
8386
|
+
const mirrors = config.mirrors;
|
|
8387
|
+
if (runsMirrors(mirrors) !== (files.mirrors !== void 0)) {
|
|
8388
|
+
throw setupError(
|
|
8389
|
+
`Server generation is corrupt: ${MIRRORS_COMPOSE_FILE} does not match the persisted server config.`
|
|
8390
|
+
);
|
|
8391
|
+
}
|
|
8392
|
+
if ((mirrors !== void 0 && mirrors.npm !== null) !== (files.npmMirrorConf !== void 0)) {
|
|
8393
|
+
throw setupError(
|
|
8394
|
+
`Server generation is corrupt: ${NPM_MIRROR_CONF_NAME} does not match the persisted server config.`
|
|
8395
|
+
);
|
|
8396
|
+
}
|
|
8397
|
+
if (dockerMirrorLogin(mirrors?.docker ?? null) === void 0 !== (secrets.dockerHubToken === void 0)) {
|
|
8398
|
+
throw setupError(
|
|
8399
|
+
"Server secrets are corrupt: a Docker Hub login needs both MEND_DOCKER_HUB_USERNAME and MEND_DOCKER_HUB_TOKEN."
|
|
8400
|
+
);
|
|
8401
|
+
}
|
|
8402
|
+
if (files.mirrors === void 0) return [];
|
|
8403
|
+
for (const service of mirrorServices(mirrors)) {
|
|
8404
|
+
if (!files.mirrors.includes(`
|
|
8405
|
+
${service}:
|
|
8406
|
+
`))
|
|
8407
|
+
throw setupError(
|
|
8408
|
+
`Server generation is corrupt: ${MIRRORS_COMPOSE_FILE} does not run the ${service} its config declares.`
|
|
8409
|
+
);
|
|
8410
|
+
}
|
|
8411
|
+
if ((mirrors !== void 0 && mirrors.docker !== null) !== (files.dockerMirrorGuard !== void 0)) {
|
|
8412
|
+
throw setupError(
|
|
8413
|
+
`Server generation is corrupt: ${DOCKER_MIRROR_GUARD_NAME} does not match the persisted server config.`
|
|
8414
|
+
);
|
|
8415
|
+
}
|
|
8416
|
+
if (files.dockerMirrorGuard !== void 0 && !files.dockerMirrorGuard.includes("registry serve /etc/distribution/config.yml"))
|
|
8417
|
+
throw setupError(
|
|
8418
|
+
`Server generation is corrupt: ${DOCKER_MIRROR_GUARD_NAME} is not the Docker mirror's guard.`
|
|
8419
|
+
);
|
|
8420
|
+
if (files.npmMirrorConf !== void 0 && !files.npmMirrorConf.includes("proxy_cache npm;"))
|
|
8421
|
+
throw setupError(
|
|
8422
|
+
`Server generation is corrupt: ${NPM_MIRROR_CONF_NAME} is not the npm mirror's configuration.`
|
|
8423
|
+
);
|
|
8424
|
+
return mirrorImagesOf(files.mirrors);
|
|
8425
|
+
};
|
|
7576
8426
|
var readServerInstallation = (store) => {
|
|
7577
8427
|
try {
|
|
7578
8428
|
const generation = storeValue(store.readActive());
|
|
@@ -7587,12 +8437,14 @@ var readServerInstallation = (store) => {
|
|
|
7587
8437
|
validateComposeAsset(generation.files.compose);
|
|
7588
8438
|
validatePostgresAsset(generation.files.postgresInit);
|
|
7589
8439
|
const edgeImage = validateOverlays(config, generation.files);
|
|
8440
|
+
const mirrorImages = validateMirrors(config, secrets, generation.files);
|
|
7590
8441
|
return {
|
|
7591
8442
|
_tag: "ok",
|
|
7592
8443
|
value: {
|
|
7593
8444
|
config,
|
|
7594
8445
|
directory: generation.directory,
|
|
7595
|
-
...edgeImage === void 0 ? {} : { edgeImage }
|
|
8446
|
+
...edgeImage === void 0 ? {} : { edgeImage },
|
|
8447
|
+
...mirrorImages.length === 0 ? {} : { mirrorImages }
|
|
7596
8448
|
}
|
|
7597
8449
|
};
|
|
7598
8450
|
} catch (cause) {
|
|
@@ -7623,7 +8475,10 @@ var readServerInstallationFacts = async (configDir) => {
|
|
|
7623
8475
|
overlays: [
|
|
7624
8476
|
...generation.files.edge === void 0 ? [] : [{ name: "compose.edge.yaml", content: generation.files.edge }],
|
|
7625
8477
|
...generation.files.caddyfile === void 0 ? [] : [{ name: "Caddyfile", content: generation.files.caddyfile }],
|
|
7626
|
-
...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }]
|
|
8478
|
+
...generation.files.posture === void 0 ? [] : [{ name: "compose.posture.yaml", content: generation.files.posture }],
|
|
8479
|
+
...generation.files.mirrors === void 0 ? [] : [{ name: MIRRORS_COMPOSE_FILE, content: generation.files.mirrors }],
|
|
8480
|
+
...generation.files.npmMirrorConf === void 0 ? [] : [{ name: NPM_MIRROR_CONF_NAME, content: generation.files.npmMirrorConf }],
|
|
8481
|
+
...generation.files.dockerMirrorGuard === void 0 ? [] : [{ name: DOCKER_MIRROR_GUARD_NAME, content: generation.files.dockerMirrorGuard }]
|
|
7627
8482
|
],
|
|
7628
8483
|
envKeys: envKeyNames(generation.files.env)
|
|
7629
8484
|
};
|
|
@@ -7635,6 +8490,7 @@ var readServerInstallationFacts = async (configDir) => {
|
|
|
7635
8490
|
};
|
|
7636
8491
|
var generationFiles = (config, secrets, assets, identity = renderIdentity(secrets)) => {
|
|
7637
8492
|
const posture = renderPostureOverlay(config);
|
|
8493
|
+
const mirrors = renderMirrorsOverlay(config.mirrors);
|
|
7638
8494
|
return {
|
|
7639
8495
|
identity,
|
|
7640
8496
|
config: `${JSON.stringify(config, null, 2)}
|
|
@@ -7642,7 +8498,10 @@ var generationFiles = (config, secrets, assets, identity = renderIdentity(secret
|
|
|
7642
8498
|
env: renderSecrets(secrets, config),
|
|
7643
8499
|
...assets,
|
|
7644
8500
|
...posture === void 0 ? {} : { posture },
|
|
7645
|
-
...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE }
|
|
8501
|
+
...config.edgeHost === void 0 ? {} : { edge: EDGE_COMPOSE_OVERLAY, caddyfile: EDGE_CADDYFILE },
|
|
8502
|
+
...mirrors === void 0 ? {} : { mirrors },
|
|
8503
|
+
...config.mirrors === void 0 || config.mirrors.npm === null ? {} : { npmMirrorConf: NPM_MIRROR_CONF },
|
|
8504
|
+
...config.mirrors === void 0 || config.mirrors.docker === null ? {} : { dockerMirrorGuard: DOCKER_MIRROR_GUARD }
|
|
7646
8505
|
};
|
|
7647
8506
|
};
|
|
7648
8507
|
var persistSetup = (store, config, secrets, assets) => storeValue(store.prepare(generationFiles(config, secrets, assets)));
|
|
@@ -7811,6 +8670,17 @@ var checkLocalImages = async (runtime, installation, policy = "local") => {
|
|
|
7811
8670
|
if (edge.status !== 0)
|
|
7812
8671
|
throw commandFailure(`Preload ${installation.edgeImage} before continuing`, edge);
|
|
7813
8672
|
}
|
|
8673
|
+
for (const mirrorImage of installation.mirrorImages ?? []) {
|
|
8674
|
+
const mirror = await inspectImage(
|
|
8675
|
+
runtime,
|
|
8676
|
+
config.dockerContext,
|
|
8677
|
+
mirrorImage,
|
|
8678
|
+
"{{.Id}}",
|
|
8679
|
+
policy
|
|
8680
|
+
);
|
|
8681
|
+
if (mirror.status !== 0)
|
|
8682
|
+
throw commandFailure(`Preload ${mirrorImage} before continuing`, mirror);
|
|
8683
|
+
}
|
|
7814
8684
|
};
|
|
7815
8685
|
var checkComposeImages = async (runtime, installation) => {
|
|
7816
8686
|
const output = await runtime.run(
|
|
@@ -7823,11 +8693,12 @@ var checkComposeImages = async (runtime, installation) => {
|
|
|
7823
8693
|
`ghcr.io/sealant-sh/mend:${installation.config.serverVersion}`,
|
|
7824
8694
|
"postgres:17-alpine",
|
|
7825
8695
|
...installation.config.bucket === "garage" ? [GARAGE_IMAGE] : [],
|
|
7826
|
-
...installation.edgeImage === void 0 ? [] : [installation.edgeImage]
|
|
8696
|
+
...installation.edgeImage === void 0 ? [] : [installation.edgeImage],
|
|
8697
|
+
...installation.mirrorImages ?? []
|
|
7827
8698
|
].toSorted();
|
|
7828
8699
|
if (images.join("\n") !== expected.join("\n")) {
|
|
7829
8700
|
throw setupError(
|
|
7830
|
-
`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}`}.`
|
|
8701
|
+
`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(", ")}`}.`
|
|
7831
8702
|
);
|
|
7832
8703
|
}
|
|
7833
8704
|
};
|
|
@@ -7874,8 +8745,16 @@ var initGarage = async (runtime, installation, secrets) => {
|
|
|
7874
8745
|
var startingNotice = (runtime, version) => runtime.writeLine(
|
|
7875
8746
|
`Starting Mend ${version} containers; Docker waits up to ${serverProcessDeadlines.composeWaitSeconds}s for them to report healthy`
|
|
7876
8747
|
);
|
|
7877
|
-
var
|
|
7878
|
-
|
|
8748
|
+
var strayServices = (config) => [
|
|
8749
|
+
...config.edgeHost === void 0 ? [{ service: "edge", label: "edge", holds: "It may still hold 80 and 443." }] : [],
|
|
8750
|
+
...config.mirrors?.npm === null ? [{ service: "npm-mirror", label: "npm mirror", holds: "It keeps running beside Mend." }] : [],
|
|
8751
|
+
...config.mirrors?.docker === null ? [{ service: "docker-mirror", label: "Docker mirror", holds: "It keeps running beside Mend." }] : []
|
|
8752
|
+
];
|
|
8753
|
+
var removeStrayServices = async (runtime, installation) => {
|
|
8754
|
+
for (const stray of strayServices(installation.config))
|
|
8755
|
+
await removeStrayService(runtime, installation, stray);
|
|
8756
|
+
};
|
|
8757
|
+
var removeStrayService = async (runtime, installation, stray) => {
|
|
7879
8758
|
const context = installation.config.dockerContext;
|
|
7880
8759
|
const listed = await runtime.run("docker", [
|
|
7881
8760
|
"--context",
|
|
@@ -7886,13 +8765,13 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7886
8765
|
"--filter",
|
|
7887
8766
|
"label=com.docker.compose.project=mend",
|
|
7888
8767
|
"--filter",
|
|
7889
|
-
|
|
8768
|
+
`label=com.docker.compose.service=${stray.service}`,
|
|
7890
8769
|
"--format",
|
|
7891
8770
|
'{{.Names}} {{.Label "com.docker.compose.project.working_dir"}}'
|
|
7892
8771
|
]);
|
|
7893
8772
|
if (listed.status !== 0 || listed.error !== void 0) {
|
|
7894
8773
|
runtime.writeLine(
|
|
7895
|
-
`Could not list this project's containers: ${outputDetail(listed)}. An
|
|
8774
|
+
`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.`
|
|
7896
8775
|
);
|
|
7897
8776
|
return;
|
|
7898
8777
|
}
|
|
@@ -7908,9 +8787,10 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7908
8787
|
const [name = "", workingDir = ""] = line.split(" ");
|
|
7909
8788
|
return { name, workingDir };
|
|
7910
8789
|
}).filter((row) => row.name !== "");
|
|
8790
|
+
const label = stray.label.charAt(0).toUpperCase() + stray.label.slice(1);
|
|
7911
8791
|
for (const row of rows.filter((candidate) => !own(candidate.workingDir))) {
|
|
7912
8792
|
runtime.writeLine(
|
|
7913
|
-
|
|
8793
|
+
`${label} container ${row.name} was started from ${row.workingDir || "an unknown directory"}, not from this installation's generations. It stays.`
|
|
7914
8794
|
);
|
|
7915
8795
|
}
|
|
7916
8796
|
const names = rows.filter((row) => own(row.workingDir)).map((row) => row.name);
|
|
@@ -7925,16 +8805,16 @@ var removeStrayEdge = async (runtime, installation) => {
|
|
|
7925
8805
|
]);
|
|
7926
8806
|
if (removed.status !== 0 || removed.error !== void 0) {
|
|
7927
8807
|
runtime.writeLine(
|
|
7928
|
-
`Could not remove the
|
|
8808
|
+
`Could not remove the ${stray.label} container ${names.join(", ")}: ${outputDetail(removed)}. ${stray.holds} To remove it: docker --context ${context} container rm --force ${names.join(" ")}`
|
|
7929
8809
|
);
|
|
7930
8810
|
return;
|
|
7931
8811
|
}
|
|
7932
8812
|
runtime.writeLine(
|
|
7933
|
-
`Removed the
|
|
8813
|
+
`Removed the ${stray.label} container ${names.join(", ")}, which this generation does not run.`
|
|
7934
8814
|
);
|
|
7935
8815
|
};
|
|
7936
8816
|
var startCompose = async (runtime, installation) => {
|
|
7937
|
-
await
|
|
8817
|
+
await removeStrayServices(runtime, installation);
|
|
7938
8818
|
startingNotice(runtime, installation.config.serverVersion);
|
|
7939
8819
|
const compose = await runtime.run(
|
|
7940
8820
|
"docker",
|
|
@@ -7974,6 +8854,48 @@ var sshPublicationLine = async (runtime, config) => {
|
|
|
7974
8854
|
return `Workspace SSH is published on ${published}. From this machine: ${observed}. Who else reaches it is up to the network and its firewall; mend operator exposure reports it as workspace-ssh.`;
|
|
7975
8855
|
};
|
|
7976
8856
|
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}.`}`;
|
|
8857
|
+
var resolveDockerHubToken = async (runtime, options, mirrors, store) => {
|
|
8858
|
+
if (dockerMirrorLogin(mirrors.docker) === void 0) return void 0;
|
|
8859
|
+
if (options.dockerHubTokenStdin) {
|
|
8860
|
+
if (runtime.readStdin === void 0)
|
|
8861
|
+
throw setupError("--docker-hub-token-stdin needs standard input, and none is available.");
|
|
8862
|
+
const token = (await runtime.readStdin()).trim();
|
|
8863
|
+
if (!isDockerHubCredential(token))
|
|
8864
|
+
throw setupError(
|
|
8865
|
+
"--docker-hub-token-stdin read no Docker Hub access token: pipe the token alone, such as a personal access token (dckr_pat_\u2026)."
|
|
8866
|
+
);
|
|
8867
|
+
return token;
|
|
8868
|
+
}
|
|
8869
|
+
const active = storeValue(store.readActive());
|
|
8870
|
+
return active === null ? void 0 : parseSecrets(active.files.env).dockerHubToken;
|
|
8871
|
+
};
|
|
8872
|
+
var mirrorsChangedLines = (before, config) => {
|
|
8873
|
+
const after = config.mirrors;
|
|
8874
|
+
if (after === void 0) return [];
|
|
8875
|
+
const lines = [];
|
|
8876
|
+
const said = (name, was, is, volume, reach) => {
|
|
8877
|
+
if (is && !was) lines.push(`The ${name} runs on this install. ${reach}`);
|
|
8878
|
+
if (was && !is)
|
|
8879
|
+
lines.push(
|
|
8880
|
+
`The ${name} is off. Its cache stays until you remove it: docker --context ${config.dockerContext} volume rm ${volume}`
|
|
8881
|
+
);
|
|
8882
|
+
};
|
|
8883
|
+
said(
|
|
8884
|
+
"npm mirror",
|
|
8885
|
+
before !== void 0 && before.npm !== null,
|
|
8886
|
+
after.npm !== null,
|
|
8887
|
+
"mend_mend-npm-mirror",
|
|
8888
|
+
`New sessions install npm packages through it, capped at ${after.npm?.maxSize ?? ""}.`
|
|
8889
|
+
);
|
|
8890
|
+
said(
|
|
8891
|
+
"Docker mirror",
|
|
8892
|
+
before !== void 0 && before.docker !== null,
|
|
8893
|
+
after.docker !== null,
|
|
8894
|
+
"mend_mend-docker-mirror",
|
|
8895
|
+
`New sessions' Docker daemons pull Docker Hub images through it (${DOCKER_MIRROR_CONTAINER}).`
|
|
8896
|
+
);
|
|
8897
|
+
return lines;
|
|
8898
|
+
};
|
|
7977
8899
|
var setupServer = async (args, runtime, store) => {
|
|
7978
8900
|
if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
|
|
7979
8901
|
throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
|
|
@@ -7999,6 +8921,7 @@ var setupServer = async (args, runtime, store) => {
|
|
|
7999
8921
|
const operatingSystem = await checkDocker(runtime, selectedContext.name);
|
|
8000
8922
|
runtime.writeLine(`Using Docker context "${selectedContext.name}" (${selectedContext.endpoint})`);
|
|
8001
8923
|
const serverVersion = await resolveServerVersion(runtime, options, existing?.config ?? null);
|
|
8924
|
+
const mirrors = resolveMirrors(existing?.config.mirrors, options);
|
|
8002
8925
|
const configWithoutBucket = {
|
|
8003
8926
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
8004
8927
|
assetContract: ASSET_CONTRACT,
|
|
@@ -8009,8 +8932,10 @@ var setupServer = async (args, runtime, store) => {
|
|
|
8009
8932
|
...selectedContext,
|
|
8010
8933
|
operatingSystem
|
|
8011
8934
|
}),
|
|
8012
|
-
...validateExposure(existing?.config ?? null, options)
|
|
8935
|
+
...validateExposure(existing?.config ?? null, options),
|
|
8936
|
+
mirrors
|
|
8013
8937
|
};
|
|
8938
|
+
const dockerHubToken = await resolveDockerHubToken(runtime, options, mirrors, store);
|
|
8014
8939
|
const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
|
|
8015
8940
|
checkSshBindAsset(configWithoutBucket, assets.compose);
|
|
8016
8941
|
const bucket = composeBucket(assets.compose);
|
|
@@ -8036,12 +8961,16 @@ var setupServer = async (args, runtime, store) => {
|
|
|
8036
8961
|
`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.`
|
|
8037
8962
|
);
|
|
8038
8963
|
}
|
|
8039
|
-
const secrets =
|
|
8964
|
+
const secrets = {
|
|
8965
|
+
...savedSecrets ?? createSecrets(runtime),
|
|
8966
|
+
...dockerHubToken === void 0 ? {} : { dockerHubToken }
|
|
8967
|
+
};
|
|
8040
8968
|
const generation = persistSetup(store, config, secrets, assets);
|
|
8041
8969
|
const installation = {
|
|
8042
8970
|
directory: generation.directory,
|
|
8043
8971
|
config,
|
|
8044
|
-
...generation.files.edge === void 0 ? {} : { edgeImage: edgeImageOf(generation.files.edge) }
|
|
8972
|
+
...generation.files.edge === void 0 ? {} : { edgeImage: edgeImageOf(generation.files.edge) },
|
|
8973
|
+
...generation.files.mirrors === void 0 ? {} : { mirrorImages: mirrorImagesOf(generation.files.mirrors) }
|
|
8045
8974
|
};
|
|
8046
8975
|
await checkComposeImages(runtime, installation);
|
|
8047
8976
|
const ownership2 = await claimServerDockerVolumes(runtime, {
|
|
@@ -8070,6 +8999,7 @@ var setupServer = async (args, runtime, store) => {
|
|
|
8070
8999
|
`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`
|
|
8071
9000
|
);
|
|
8072
9001
|
}
|
|
9002
|
+
for (const line of mirrorsChangedLines(existing?.config.mirrors, config)) runtime.writeLine(line);
|
|
8073
9003
|
runtime.writeLine(
|
|
8074
9004
|
`Open ${config.appUrl}, create the first account, then run: mend login --url ${config.appUrl}`
|
|
8075
9005
|
);
|
|
@@ -8134,7 +9064,7 @@ var interruptionNotice = (runtime) => runtime.writeLine(
|
|
|
8134
9064
|
"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."
|
|
8135
9065
|
);
|
|
8136
9066
|
var startInstallation = async (runtime, installation, secrets) => {
|
|
8137
|
-
await
|
|
9067
|
+
await removeStrayServices(runtime, installation);
|
|
8138
9068
|
startingNotice(runtime, installation.config.serverVersion);
|
|
8139
9069
|
await composeCommand(runtime, installation, [
|
|
8140
9070
|
"up",
|
|
@@ -8182,7 +9112,7 @@ var parseUpgradeOptions = (args) => {
|
|
|
8182
9112
|
);
|
|
8183
9113
|
return { ...options, fromPreview, keepBackups };
|
|
8184
9114
|
};
|
|
8185
|
-
var
|
|
9115
|
+
var formatBytes4 = (bytes) => {
|
|
8186
9116
|
if (bytes < 1024) return `${bytes} B`;
|
|
8187
9117
|
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
8188
9118
|
let value = bytes / 1024;
|
|
@@ -8225,11 +9155,11 @@ var pruneUpgradeBackups = (runtime, store, backup, keep) => {
|
|
|
8225
9155
|
const { removed, kept, held, failed, unsynced } = pruned.value;
|
|
8226
9156
|
for (const entry of removed)
|
|
8227
9157
|
runtime.writeLine(
|
|
8228
|
-
`Removed upgrade backup ${entry.directory} \xB7 ${
|
|
9158
|
+
`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" : ""}`
|
|
8229
9159
|
);
|
|
8230
9160
|
const freed = removed.reduce((total, entry) => total + entry.bytes, 0);
|
|
8231
9161
|
runtime.writeLine(
|
|
8232
|
-
`Upgrade backups \xB7 removed ${removed.length}${removed.length > 0 ? ` \xB7 ${
|
|
9162
|
+
`Upgrade backups \xB7 removed ${removed.length}${removed.length > 0 ? ` \xB7 ${formatBytes4(freed)} freed` : ""} \xB7 kept ${kept.length} (--keep-backups ${keep})`
|
|
8233
9163
|
);
|
|
8234
9164
|
for (const entry of failed)
|
|
8235
9165
|
runtime.writeLine(`Could not remove upgrade backup ${entry.directory}: ${entry.message}`);
|
|
@@ -8398,14 +9328,23 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8398
9328
|
...carried,
|
|
8399
9329
|
assetContract: ASSET_CONTRACT,
|
|
8400
9330
|
serverVersion: version,
|
|
8401
|
-
...bucket === void 0 ? {} : { bucket }
|
|
9331
|
+
...bucket === void 0 ? {} : { bucket },
|
|
9332
|
+
// An install from before the mirrors gains both here; one that turned a mirror off keeps it off.
|
|
9333
|
+
mirrors: carried.mirrors ?? DEFAULT_MIRRORS
|
|
9334
|
+
};
|
|
9335
|
+
const previousToken = parseSecrets(previous.files.env).dockerHubToken;
|
|
9336
|
+
const secrets = {
|
|
9337
|
+
...parseSecrets(previous.files.identity),
|
|
9338
|
+
...previousToken === void 0 ? {} : { dockerHubToken: previousToken }
|
|
8402
9339
|
};
|
|
8403
|
-
const secrets = parseSecrets(previous.files.identity);
|
|
8404
9340
|
const files = generationFiles(config, secrets, assets, previous.files.identity);
|
|
8405
9341
|
const parsed = parseServerConfig(files.config);
|
|
8406
9342
|
if (renderSecrets(parseSecrets(files.env), parsed) !== files.env)
|
|
8407
9343
|
throw setupError("Invalid upgrade configuration.");
|
|
8408
|
-
const edgeImage =
|
|
9344
|
+
const edgeImage = {
|
|
9345
|
+
...files.edge === void 0 ? {} : { edgeImage: edgeImageOf(files.edge) },
|
|
9346
|
+
...files.mirrors === void 0 ? {} : { mirrorImages: mirrorImagesOf(files.mirrors) }
|
|
9347
|
+
};
|
|
8409
9348
|
await checkLocalImages(
|
|
8410
9349
|
runtime,
|
|
8411
9350
|
{ config, ...edgeImage },
|
|
@@ -8490,6 +9429,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
|
|
|
8490
9429
|
);
|
|
8491
9430
|
}
|
|
8492
9431
|
runtime.writeLine(`Upgraded to ${version}. Retained database backup: ${backup.directory}`);
|
|
9432
|
+
for (const line of mirrorsChangedLines(existing.config.mirrors, config)) runtime.writeLine(line);
|
|
8493
9433
|
pruneUpgradeBackups(runtime, store, backup, options.keepBackups);
|
|
8494
9434
|
};
|
|
8495
9435
|
var observeEdgeCertificate = async (runtime, installation, host) => {
|
|
@@ -8593,6 +9533,75 @@ var exposureReportOf = (value) => {
|
|
|
8593
9533
|
const parsed = items.flatMap(exposureItemOf);
|
|
8594
9534
|
return parsed.length === items.length && parsed.length > 0 ? { declared, items: parsed } : null;
|
|
8595
9535
|
};
|
|
9536
|
+
var mirrorStatusLines = async (runtime, installation, runningServices) => {
|
|
9537
|
+
const mirrors = installation.config.mirrors;
|
|
9538
|
+
if (mirrors === void 0) return [];
|
|
9539
|
+
const run2 = (args) => runtime.run("docker", serverComposeArgs(composeTarget(installation), args), {
|
|
9540
|
+
timeoutMs: serverProcessDeadlines.ordinary
|
|
9541
|
+
});
|
|
9542
|
+
const failed = (output) => output.status !== 0 || output.error !== void 0 ? outputDetail(output) : null;
|
|
9543
|
+
const diskOf = async (service, directory) => {
|
|
9544
|
+
const probe = await run2(["exec", "-T", service, "sh", "-c", mirrorDiskProbe(directory)]);
|
|
9545
|
+
return failed(probe) ?? mirrorDiskOf(probe.stdout) ?? "du and df answered nothing readable";
|
|
9546
|
+
};
|
|
9547
|
+
const lines = [];
|
|
9548
|
+
const npmRunning = runningServices.includes("npm-mirror");
|
|
9549
|
+
if (mirrors.npm === null || !npmRunning) {
|
|
9550
|
+
lines.push(observedNpmMirrorLine(mirrors, { running: false, disk: "", traffic: "" }));
|
|
9551
|
+
} else {
|
|
9552
|
+
const log = await run2([
|
|
9553
|
+
"logs",
|
|
9554
|
+
"--no-color",
|
|
9555
|
+
"--no-log-prefix",
|
|
9556
|
+
"--since",
|
|
9557
|
+
"24h",
|
|
9558
|
+
"npm-mirror"
|
|
9559
|
+
]);
|
|
9560
|
+
lines.push(
|
|
9561
|
+
observedNpmMirrorLine(mirrors, {
|
|
9562
|
+
running: true,
|
|
9563
|
+
disk: await diskOf("npm-mirror", "/var/cache/npm-mirror"),
|
|
9564
|
+
traffic: failed(log) ?? npmMirrorTraffic(log.stdout)
|
|
9565
|
+
})
|
|
9566
|
+
);
|
|
9567
|
+
}
|
|
9568
|
+
const dockerRunning = runningServices.includes("docker-mirror");
|
|
9569
|
+
if (mirrors.docker === null || !dockerRunning) {
|
|
9570
|
+
lines.push(
|
|
9571
|
+
observedDockerMirrorLine(mirrors, { running: false, disk: "", traffic: "", startedAt: null })
|
|
9572
|
+
);
|
|
9573
|
+
} else {
|
|
9574
|
+
const metrics = await run2([
|
|
9575
|
+
"exec",
|
|
9576
|
+
"-T",
|
|
9577
|
+
"docker-mirror",
|
|
9578
|
+
"wget",
|
|
9579
|
+
"-q",
|
|
9580
|
+
"-O",
|
|
9581
|
+
"-",
|
|
9582
|
+
"http://127.0.0.1:5001/metrics"
|
|
9583
|
+
]);
|
|
9584
|
+
const started = await runtime.run("docker", [
|
|
9585
|
+
"--context",
|
|
9586
|
+
installation.config.dockerContext,
|
|
9587
|
+
"container",
|
|
9588
|
+
"inspect",
|
|
9589
|
+
"--format",
|
|
9590
|
+
"{{.State.StartedAt}}",
|
|
9591
|
+
DOCKER_MIRROR_CONTAINER
|
|
9592
|
+
]);
|
|
9593
|
+
const startedAt = failed(started) === null ? started.stdout.trim().replace(/\.\d+Z$/, "Z") : "";
|
|
9594
|
+
lines.push(
|
|
9595
|
+
observedDockerMirrorLine(mirrors, {
|
|
9596
|
+
running: true,
|
|
9597
|
+
disk: await diskOf("docker-mirror", "/var/lib/registry"),
|
|
9598
|
+
traffic: failed(metrics) ?? dockerMirrorTraffic(metrics.stdout) ?? "the registry reported no proxy counters",
|
|
9599
|
+
startedAt: startedAt === "" ? null : startedAt
|
|
9600
|
+
})
|
|
9601
|
+
);
|
|
9602
|
+
}
|
|
9603
|
+
return lines;
|
|
9604
|
+
};
|
|
8596
9605
|
var serverStatus = async (runtime, installation) => {
|
|
8597
9606
|
const { config } = installation;
|
|
8598
9607
|
runtime.writeLine(`Pinned Mend ${config.serverVersion} at ${config.appUrl}`);
|
|
@@ -8616,6 +9625,8 @@ var serverStatus = async (runtime, installation) => {
|
|
|
8616
9625
|
})
|
|
8617
9626
|
);
|
|
8618
9627
|
}
|
|
9628
|
+
for (const line of await mirrorStatusLines(runtime, installation, runningServices))
|
|
9629
|
+
runtime.writeLine(line);
|
|
8619
9630
|
if (!runningServices.includes("mend")) {
|
|
8620
9631
|
runtime.writeLine("Mend is stopped. No health claim was made.");
|
|
8621
9632
|
return;
|
|
@@ -8684,7 +9695,10 @@ var manageServer = async (command, args, runtime, store) => {
|
|
|
8684
9695
|
"Mend",
|
|
8685
9696
|
"Postgres",
|
|
8686
9697
|
...installation.config.bucket === "garage" ? ["Garage"] : [],
|
|
8687
|
-
...installation.config.edgeHost === void 0 ? [] : ["the edge"]
|
|
9698
|
+
...installation.config.edgeHost === void 0 ? [] : ["the edge"],
|
|
9699
|
+
...mirrorServices(installation.config.mirrors).map(
|
|
9700
|
+
(service) => service === "npm-mirror" ? "the npm mirror" : "the Docker mirror"
|
|
9701
|
+
)
|
|
8688
9702
|
];
|
|
8689
9703
|
runtime.writeLine(
|
|
8690
9704
|
`${stopped.slice(0, -1).join(", ")} and ${stopped.at(-1)} stopped. Volumes, configuration and workspace containers are retained.`
|
|
@@ -8728,7 +9742,13 @@ var nodeServerRuntime = () => {
|
|
|
8728
9742
|
`),
|
|
8729
9743
|
dockerDaemonFacts: hostDockerDaemonFacts,
|
|
8730
9744
|
readLogin: (configDir) => savedLogin(configDir, environment),
|
|
8731
|
-
probeSsh: probeSshFromHere
|
|
9745
|
+
probeSsh: probeSshFromHere,
|
|
9746
|
+
readStdin: async () => {
|
|
9747
|
+
const chunks = [];
|
|
9748
|
+
for await (const chunk of process.stdin)
|
|
9749
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
9750
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9751
|
+
}
|
|
8732
9752
|
};
|
|
8733
9753
|
};
|
|
8734
9754
|
var sshBannerAt = async (host, port, timeoutMs) => {
|
|
@@ -9311,6 +10331,7 @@ var scanSkillLibrary = (root) => {
|
|
|
9311
10331
|
};
|
|
9312
10332
|
|
|
9313
10333
|
// src/ssh-setup.ts
|
|
10334
|
+
import * as fs19 from "node:fs";
|
|
9314
10335
|
import * as os12 from "node:os";
|
|
9315
10336
|
import * as path19 from "node:path";
|
|
9316
10337
|
|
|
@@ -9837,6 +10858,164 @@ var setup = async (api2, cliHome, serverUrl, args) => {
|
|
|
9837
10858
|
`connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${parsedTarget.value.alias} ${dim5("\xB7 the VS Code extension uses this automatically")}`
|
|
9838
10859
|
);
|
|
9839
10860
|
};
|
|
10861
|
+
var errorText = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
10862
|
+
var thisMachineKeys = (view, cliHome, serverUrl) => {
|
|
10863
|
+
const fingerprints = /* @__PURE__ */ new Set();
|
|
10864
|
+
const unreadable = [];
|
|
10865
|
+
const target = parseWorkspaceSshTarget({
|
|
10866
|
+
serverUrl,
|
|
10867
|
+
publishedPort: view?.gateway?.port ?? 22
|
|
10868
|
+
});
|
|
10869
|
+
const config = readWorkspaceSshConfig(sshConfigPath());
|
|
10870
|
+
const configured = target.ok && config.ok ? configuredWorkspaceSshIdentityFile(config.value, target.value) : null;
|
|
10871
|
+
const publicPaths = /* @__PURE__ */ new Set();
|
|
10872
|
+
if (configured !== null) {
|
|
10873
|
+
publicPaths.add(configured.endsWith(".pub") ? configured : `${configured}.pub`);
|
|
10874
|
+
}
|
|
10875
|
+
const keyDirectory = path19.join(cliHome, "ssh");
|
|
10876
|
+
try {
|
|
10877
|
+
for (const entry of fs19.readdirSync(keyDirectory)) {
|
|
10878
|
+
if (entry.endsWith(".pub")) publicPaths.add(path19.join(keyDirectory, entry));
|
|
10879
|
+
}
|
|
10880
|
+
} catch (cause) {
|
|
10881
|
+
const code = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
10882
|
+
if (code !== "ENOENT") unreadable.push(`${keyDirectory}: ${errorText(cause)}`);
|
|
10883
|
+
}
|
|
10884
|
+
let missingPublicHalf = null;
|
|
10885
|
+
for (const publicPath of publicPaths) {
|
|
10886
|
+
let line;
|
|
10887
|
+
try {
|
|
10888
|
+
line = fs19.readFileSync(publicPath, "utf8");
|
|
10889
|
+
} catch (cause) {
|
|
10890
|
+
const code = cause instanceof Error && "code" in cause ? cause.code : void 0;
|
|
10891
|
+
if (code === "ENOENT") missingPublicHalf = publicPath;
|
|
10892
|
+
else unreadable.push(`${publicPath}: ${errorText(cause)}`);
|
|
10893
|
+
continue;
|
|
10894
|
+
}
|
|
10895
|
+
const fingerprint = workspaceSshPublicKeyFingerprint(line);
|
|
10896
|
+
if (fingerprint.ok) fingerprints.add(fingerprint.value);
|
|
10897
|
+
else unreadable.push(`${publicPath}: ${fingerprint.error.message}`);
|
|
10898
|
+
}
|
|
10899
|
+
if (missingPublicHalf !== null) {
|
|
10900
|
+
const picked = pickWorkspaceSshKey({
|
|
10901
|
+
configHome: cliHome,
|
|
10902
|
+
configuredIdentityFile: configured,
|
|
10903
|
+
create: false
|
|
10904
|
+
});
|
|
10905
|
+
if (picked.ok && picked.value !== null) fingerprints.add(picked.value.fingerprint);
|
|
10906
|
+
else {
|
|
10907
|
+
unreadable.push(
|
|
10908
|
+
`${missingPublicHalf}: missing${picked.ok ? "" : `, and ${picked.error.message}`}`
|
|
10909
|
+
);
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
return { fingerprints, unreadable };
|
|
10913
|
+
};
|
|
10914
|
+
var findKey = (keys, wanted) => {
|
|
10915
|
+
const fingerprint = wanted.startsWith("SHA256:") ? wanted : `SHA256:${wanted}`;
|
|
10916
|
+
return keys.find((key) => key.fingerprint === fingerprint || key.sshKeyId === wanted);
|
|
10917
|
+
};
|
|
10918
|
+
var REMOVED_KEY_EFFECT = "the gateway refuses it from the next connection; a connection already open stays open until it ends";
|
|
10919
|
+
var listKeys = async (api2, cliHome, serverUrl, args) => {
|
|
10920
|
+
const view = await api2("GET", "/workspace-ssh");
|
|
10921
|
+
const local = thisMachineKeys(view, cliHome, serverUrl).fingerprints;
|
|
10922
|
+
if (args.includes("--json")) {
|
|
10923
|
+
console.log(
|
|
10924
|
+
JSON.stringify(
|
|
10925
|
+
view.keys.map((key) => ({ ...key, thisMachine: local.has(key.fingerprint) })),
|
|
10926
|
+
null,
|
|
10927
|
+
2
|
|
10928
|
+
)
|
|
10929
|
+
);
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10932
|
+
if (view.keys.length === 0) {
|
|
10933
|
+
say5(`no workspace ssh keys registered ${dim5("\xB7 run: mend ssh setup")}`);
|
|
10934
|
+
return;
|
|
10935
|
+
}
|
|
10936
|
+
const width = Math.max(...view.keys.map((key) => key.name.length));
|
|
10937
|
+
for (const key of view.keys) {
|
|
10938
|
+
say5(
|
|
10939
|
+
`${key.fingerprint} ${key.name.padEnd(width)} ${dim5(`${key.algorithm} \xB7 registered ${key.createdAt.slice(0, 10)}`)}${local.has(key.fingerprint) ? ` ${green5("\u25CF this machine")}` : ""}`
|
|
10940
|
+
);
|
|
10941
|
+
}
|
|
10942
|
+
};
|
|
10943
|
+
var removeKey = async (api2, cliHome, serverUrl, args) => {
|
|
10944
|
+
const [wanted] = args;
|
|
10945
|
+
if (wanted === void 0 || wanted.startsWith("-")) {
|
|
10946
|
+
return showFailure("usage: mend ssh keys remove <fingerprint> \xB7 mend ssh keys lists them");
|
|
10947
|
+
}
|
|
10948
|
+
const view = await api2("GET", "/workspace-ssh");
|
|
10949
|
+
const key = findKey(view.keys, wanted);
|
|
10950
|
+
if (key === void 0) {
|
|
10951
|
+
return showFailure(
|
|
10952
|
+
`none of your registered keys has fingerprint ${wanted} \xB7 mend ssh keys lists them`
|
|
10953
|
+
);
|
|
10954
|
+
}
|
|
10955
|
+
const removed = await api2(
|
|
10956
|
+
"DELETE",
|
|
10957
|
+
`/workspace-ssh/keys/${encodeURIComponent(key.sshKeyId)}`
|
|
10958
|
+
);
|
|
10959
|
+
say5(`removed ${removed.fingerprint} ${dim5(`\xB7 ${removed.name}`)}`);
|
|
10960
|
+
say5(dim5(REMOVED_KEY_EFFECT));
|
|
10961
|
+
if (thisMachineKeys(view, cliHome, serverUrl).fingerprints.has(removed.fingerprint)) {
|
|
10962
|
+
say5(
|
|
10963
|
+
dim5(
|
|
10964
|
+
"this machine's key: the file and the ~/.ssh/config block stay; mend ssh setup registers it again"
|
|
10965
|
+
)
|
|
10966
|
+
);
|
|
10967
|
+
}
|
|
10968
|
+
};
|
|
10969
|
+
var removeThisMachineKey = async (api2, cliHome, serverUrl) => {
|
|
10970
|
+
let view;
|
|
10971
|
+
try {
|
|
10972
|
+
view = await api2("GET", "/workspace-ssh");
|
|
10973
|
+
} catch (cause) {
|
|
10974
|
+
const local2 = thisMachineKeys(null, cliHome, serverUrl);
|
|
10975
|
+
return {
|
|
10976
|
+
removed: [],
|
|
10977
|
+
stillActive: [...local2.fingerprints],
|
|
10978
|
+
problem: `the server's key list could not be read: ${errorText(cause)}`
|
|
10979
|
+
};
|
|
10980
|
+
}
|
|
10981
|
+
const local = thisMachineKeys(view, cliHome, serverUrl);
|
|
10982
|
+
const removed = [];
|
|
10983
|
+
const stillActive = [];
|
|
10984
|
+
const problems = [];
|
|
10985
|
+
for (const key of view.keys.filter(
|
|
10986
|
+
(candidate) => local.fingerprints.has(candidate.fingerprint)
|
|
10987
|
+
)) {
|
|
10988
|
+
try {
|
|
10989
|
+
await api2("DELETE", `/workspace-ssh/keys/${encodeURIComponent(key.sshKeyId)}`);
|
|
10990
|
+
removed.push(key.fingerprint);
|
|
10991
|
+
} catch (cause) {
|
|
10992
|
+
stillActive.push(key.fingerprint);
|
|
10993
|
+
problems.push(`${key.fingerprint} was not removed: ${errorText(cause)}`);
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
if (local.unreadable.length > 0) {
|
|
10997
|
+
problems.push(
|
|
10998
|
+
`this machine's key could not be read (${local.unreadable.join("; ")}), so Mend cannot tell which registered key is this machine's`
|
|
10999
|
+
);
|
|
11000
|
+
}
|
|
11001
|
+
return { removed, stillActive, problem: problems.length === 0 ? null : problems.join("; ") };
|
|
11002
|
+
};
|
|
11003
|
+
var keysCommand = async (api2, cliHome, serverUrl, args) => {
|
|
11004
|
+
const [subcommand, ...rest] = args;
|
|
11005
|
+
switch (subcommand) {
|
|
11006
|
+
case void 0:
|
|
11007
|
+
case "list":
|
|
11008
|
+
case "--json":
|
|
11009
|
+
return listKeys(api2, cliHome, serverUrl, args);
|
|
11010
|
+
case "remove":
|
|
11011
|
+
case "rm":
|
|
11012
|
+
return removeKey(api2, cliHome, serverUrl, rest);
|
|
11013
|
+
default:
|
|
11014
|
+
showFailure(
|
|
11015
|
+
`Unknown ssh keys subcommand "${subcommand}". Try: mend ssh keys [--json] \xB7 mend ssh keys remove <fingerprint>`
|
|
11016
|
+
);
|
|
11017
|
+
}
|
|
11018
|
+
};
|
|
9840
11019
|
var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
9841
11020
|
const [subcommand, ...rest] = args;
|
|
9842
11021
|
switch (subcommand) {
|
|
@@ -9845,15 +11024,17 @@ var sshCommand = async (args, api2, cliHome, serverUrl) => {
|
|
|
9845
11024
|
return showStatus(api2, cliHome, serverUrl, rest);
|
|
9846
11025
|
case "setup":
|
|
9847
11026
|
return setup(api2, cliHome, serverUrl, rest);
|
|
11027
|
+
case "keys":
|
|
11028
|
+
return keysCommand(api2, cliHome, serverUrl, rest);
|
|
9848
11029
|
default:
|
|
9849
11030
|
showFailure(
|
|
9850
|
-
`Unknown ssh subcommand "${subcommand}". Try: mend ssh \xB7 mend ssh setup [--key <path>] [--host <hostname>]`
|
|
11031
|
+
`Unknown ssh subcommand "${subcommand}". Try: mend ssh \xB7 mend ssh setup [--key <path>] [--host <hostname>] \xB7 mend ssh keys [remove <fingerprint>]`
|
|
9851
11032
|
);
|
|
9852
11033
|
}
|
|
9853
11034
|
};
|
|
9854
11035
|
|
|
9855
11036
|
// src/uninstall.ts
|
|
9856
|
-
import * as
|
|
11037
|
+
import * as fs20 from "node:fs";
|
|
9857
11038
|
import * as path20 from "node:path";
|
|
9858
11039
|
var UNINSTALL_USAGE = "usage: mend uninstall [--all | --server | --home] [--yes]";
|
|
9859
11040
|
var parseUninstallArgs = (args) => {
|
|
@@ -9876,14 +11057,14 @@ var parseUninstallArgs = (args) => {
|
|
|
9876
11057
|
var SERVER_FILES = ["identity.env", "active", "generations", "backups"];
|
|
9877
11058
|
var countEntries = (directory) => {
|
|
9878
11059
|
try {
|
|
9879
|
-
return
|
|
11060
|
+
return fs20.readdirSync(directory).length;
|
|
9880
11061
|
} catch {
|
|
9881
11062
|
return 0;
|
|
9882
11063
|
}
|
|
9883
11064
|
};
|
|
9884
11065
|
var exists = (file) => {
|
|
9885
11066
|
try {
|
|
9886
|
-
|
|
11067
|
+
fs20.lstatSync(file);
|
|
9887
11068
|
return true;
|
|
9888
11069
|
} catch {
|
|
9889
11070
|
return false;
|
|
@@ -9892,7 +11073,7 @@ var exists = (file) => {
|
|
|
9892
11073
|
var managedBlockCount = (sshConfigFile) => {
|
|
9893
11074
|
let config;
|
|
9894
11075
|
try {
|
|
9895
|
-
config =
|
|
11076
|
+
config = fs20.readFileSync(sshConfigFile, "utf8");
|
|
9896
11077
|
} catch {
|
|
9897
11078
|
return 0;
|
|
9898
11079
|
}
|
|
@@ -9930,6 +11111,7 @@ var describeUninstall = async (runtime, scope) => {
|
|
|
9930
11111
|
appUrl: read.value.config.appUrl,
|
|
9931
11112
|
dockerContext: read.value.config.dockerContext,
|
|
9932
11113
|
edgeHost: read.value.config.edgeHost ?? null,
|
|
11114
|
+
mirrors: mirrorServices(read.value.config.mirrors),
|
|
9933
11115
|
generations: countEntries(path20.join(configDir, "generations")),
|
|
9934
11116
|
backups: countEntries(path20.join(configDir, "backups"))
|
|
9935
11117
|
};
|
|
@@ -9959,8 +11141,12 @@ var planLines = (plan, configDir) => {
|
|
|
9959
11141
|
`server Mend ${version}${appUrl === "" ? "" : ` at ${appUrl}`}${dockerContext === "" ? "" : ` \xB7 docker context ${dockerContext}`}`
|
|
9960
11142
|
);
|
|
9961
11143
|
if (dockerContext !== "") {
|
|
9962
|
-
const
|
|
9963
|
-
const
|
|
11144
|
+
const mirrors = plan.server.mirrors ?? [];
|
|
11145
|
+
const edge = [...edgeHost === null ? [] : ["edge"], ...mirrors];
|
|
11146
|
+
const edgeVolumes = [
|
|
11147
|
+
...edgeHost === null ? [] : ["mend-edge-data", "mend-edge-config"],
|
|
11148
|
+
...mirrors.map((service) => `mend-${service}`)
|
|
11149
|
+
];
|
|
9964
11150
|
lines.push(
|
|
9965
11151
|
` 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}`}`
|
|
9966
11152
|
);
|
|
@@ -9980,6 +11166,9 @@ var planLines = (plan, configDir) => {
|
|
|
9980
11166
|
if (plan.home.managedSshBlocks > 0) {
|
|
9981
11167
|
parts.push(`${plural(plan.home.managedSshBlocks, "managed block")} in ~/.ssh/config`);
|
|
9982
11168
|
}
|
|
11169
|
+
if (plan.home.signedIn !== null) {
|
|
11170
|
+
parts.push(`this machine's workspace ssh key on ${plan.home.signedIn.url}, if registered`);
|
|
11171
|
+
}
|
|
9983
11172
|
lines.push(parts.length === 0 ? "home nothing of Mend's here" : `home ${parts[0]}`);
|
|
9984
11173
|
for (const part of parts.slice(1)) lines.push(` ${part}`);
|
|
9985
11174
|
}
|
|
@@ -10019,7 +11208,9 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
10019
11208
|
`docker compose down failed: ${(down.error ?? down.stderr.trim()) || "no output"}. Containers and files are retained; fix Docker and run mend uninstall again.`
|
|
10020
11209
|
);
|
|
10021
11210
|
}
|
|
10022
|
-
server.writeLine(
|
|
11211
|
+
server.writeLine(
|
|
11212
|
+
`removed containers ${["mend", "postgres", "garage", ...mirrorServices(installation.config.mirrors)].join(", ")} and the Compose-owned volumes`
|
|
11213
|
+
);
|
|
10023
11214
|
const namespace = installation.config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
|
|
10024
11215
|
const dataVolumes = [namespace.store, ...secondaryVolumesOf(namespace)];
|
|
10025
11216
|
const identity = store.readIdentity();
|
|
@@ -10073,7 +11264,7 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
10073
11264
|
}
|
|
10074
11265
|
}
|
|
10075
11266
|
for (const name of SERVER_FILES) {
|
|
10076
|
-
|
|
11267
|
+
fs20.rmSync(path20.join(configDir, name), { recursive: true, force: true });
|
|
10077
11268
|
}
|
|
10078
11269
|
server.writeLine(`removed ${configDir}/{identity.env, active, generations, backups}`);
|
|
10079
11270
|
},
|
|
@@ -10084,20 +11275,20 @@ var removeServer = async (runtime, failures, leftovers) => {
|
|
|
10084
11275
|
var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
10085
11276
|
const { server } = runtime;
|
|
10086
11277
|
if (plan.cliConfig !== null) {
|
|
10087
|
-
|
|
11278
|
+
fs20.rmSync(plan.cliConfig, { force: true });
|
|
10088
11279
|
server.writeLine(`removed ${plan.cliConfig}`);
|
|
10089
11280
|
}
|
|
10090
11281
|
if (plan.sshDirectory !== null) {
|
|
10091
|
-
|
|
11282
|
+
fs20.rmSync(plan.sshDirectory, { recursive: true, force: true });
|
|
10092
11283
|
server.writeLine(`removed ${plan.sshDirectory}`);
|
|
10093
11284
|
}
|
|
10094
11285
|
if (plan.managedSshBlocks > 0) {
|
|
10095
11286
|
try {
|
|
10096
|
-
const before =
|
|
11287
|
+
const before = fs20.readFileSync(runtime.sshConfigFile, "utf8");
|
|
10097
11288
|
const stripped = stripManagedWorkspaceSshBlocks(before);
|
|
10098
11289
|
if (!stripped.ok) throw stripped.error;
|
|
10099
11290
|
if (stripped.value.removed > 0) {
|
|
10100
|
-
|
|
11291
|
+
fs20.writeFileSync(runtime.sshConfigFile, stripped.value.config, { mode: 384 });
|
|
10101
11292
|
server.writeLine(
|
|
10102
11293
|
`removed ${plural(stripped.value.removed, "managed block")} from ${runtime.sshConfigFile}`
|
|
10103
11294
|
);
|
|
@@ -10110,12 +11301,12 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
10110
11301
|
}
|
|
10111
11302
|
let remaining = [];
|
|
10112
11303
|
try {
|
|
10113
|
-
remaining =
|
|
11304
|
+
remaining = fs20.readdirSync(runtime.cliHome);
|
|
10114
11305
|
} catch {
|
|
10115
11306
|
return;
|
|
10116
11307
|
}
|
|
10117
11308
|
if (remaining.length === 0) {
|
|
10118
|
-
|
|
11309
|
+
fs20.rmdirSync(runtime.cliHome);
|
|
10119
11310
|
server.writeLine(`removed ${runtime.cliHome}`);
|
|
10120
11311
|
} else {
|
|
10121
11312
|
leftovers.push(
|
|
@@ -10126,6 +11317,21 @@ var removeHome = async (runtime, plan, failures, leftovers) => {
|
|
|
10126
11317
|
var executeUninstall = async (runtime, plan) => {
|
|
10127
11318
|
const failures = [];
|
|
10128
11319
|
const leftovers = [];
|
|
11320
|
+
if (plan.home !== null && plan.home.signedIn !== null) {
|
|
11321
|
+
const { url } = plan.home.signedIn;
|
|
11322
|
+
const removal = await runtime.removeWorkspaceSshKey();
|
|
11323
|
+
for (const fingerprint of removal.removed) {
|
|
11324
|
+
runtime.server.writeLine(
|
|
11325
|
+
`removed workspace ssh key ${fingerprint} on ${url} \xB7 the gateway refuses it from the next connection`
|
|
11326
|
+
);
|
|
11327
|
+
}
|
|
11328
|
+
if (removal.problem !== null) {
|
|
11329
|
+
const [first] = removal.stillActive;
|
|
11330
|
+
failures.push(
|
|
11331
|
+
first === void 0 ? `this machine's workspace ssh key on ${url} may still be registered: ${removal.problem}. From another signed-in machine, mend ssh keys lists your keys and mend ssh keys remove <fingerprint> removes one, or use Settings \u2192 Workspace SSH` : `workspace ssh key ${removal.stillActive.join(", ")} is still registered on ${url}: ${removal.problem}. From another signed-in machine: mend ssh keys remove ${first}, or use Settings \u2192 Workspace SSH`
|
|
11332
|
+
);
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
10129
11335
|
if (plan.home?.signedIn !== null && plan.home?.signedIn?.deviceId != null) {
|
|
10130
11336
|
const failure2 = await runtime.revokeDevice();
|
|
10131
11337
|
if (failure2 === null) {
|
|
@@ -10324,14 +11530,14 @@ var mendCliHome = () => {
|
|
|
10324
11530
|
"mend"
|
|
10325
11531
|
);
|
|
10326
11532
|
const legacy = path21.join(os13.homedir(), ".mend");
|
|
10327
|
-
return !
|
|
11533
|
+
return !fs21.existsSync(preferred) && fs21.existsSync(legacy) ? legacy : preferred;
|
|
10328
11534
|
};
|
|
10329
11535
|
var CONFIG_PATH = path21.join(mendCliHome(), "cli.json");
|
|
10330
11536
|
var loadConfig = () => {
|
|
10331
11537
|
let fileConfig = {};
|
|
10332
|
-
if (
|
|
11538
|
+
if (fs21.existsSync(CONFIG_PATH)) {
|
|
10333
11539
|
try {
|
|
10334
|
-
fileConfig = JSON.parse(
|
|
11540
|
+
fileConfig = JSON.parse(fs21.readFileSync(CONFIG_PATH, "utf8"));
|
|
10335
11541
|
} catch {
|
|
10336
11542
|
fail7(`could not parse ${CONFIG_PATH}`);
|
|
10337
11543
|
}
|
|
@@ -11468,8 +12674,9 @@ var printService = (config, service) => {
|
|
|
11468
12674
|
const status = service.status === "reachable" ? green7(service.status) : amber2(service.status);
|
|
11469
12675
|
const port = `:${service.workspacePort ?? "?"}${service.protocol === "udp" ? "/udp" : ""}`;
|
|
11470
12676
|
const statusPad = " ".repeat(Math.max(1, 12 - service.status.length));
|
|
12677
|
+
const processLine2 = service.processId === null ? "no process" : `process ${service.processId.slice(0, 8)}`;
|
|
11471
12678
|
say7(
|
|
11472
|
-
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim7(port.padEnd(7))} ${dim7(service.id.slice(0, 8))}`
|
|
12679
|
+
`${(service.label ?? service.id.slice(0, 8)).padEnd(14)} ${status}${statusPad}${dim7(port.padEnd(7))} ${dim7(`service ${service.id.slice(0, 8)} \xB7 ${processLine2}`)}`
|
|
11473
12680
|
);
|
|
11474
12681
|
printServiceAccess(config, service);
|
|
11475
12682
|
printWorkspaceTtlFailure(service);
|
|
@@ -11512,8 +12719,30 @@ var serviceAdd = async (config, args) => {
|
|
|
11512
12719
|
say7(dim7(` nothing answered on :${port} yet \u2014 the URL goes live when something listens`));
|
|
11513
12720
|
}
|
|
11514
12721
|
};
|
|
11515
|
-
var serviceList = async (config) => {
|
|
12722
|
+
var serviceList = async (config, args) => {
|
|
12723
|
+
const unknown = args.find((arg) => arg !== "--json");
|
|
12724
|
+
if (unknown !== void 0) {
|
|
12725
|
+
return fail7(`unknown argument ${unknown} \xB7 ${usageOf("service list")}`);
|
|
12726
|
+
}
|
|
11516
12727
|
const services = await fetchServices(config);
|
|
12728
|
+
if (args.includes("--json")) {
|
|
12729
|
+
printJson({
|
|
12730
|
+
version: 1,
|
|
12731
|
+
services: services.map((service) => ({
|
|
12732
|
+
id: service.id,
|
|
12733
|
+
name: service.label,
|
|
12734
|
+
sessionId: service.sessionId,
|
|
12735
|
+
processId: service.processId,
|
|
12736
|
+
status: service.status,
|
|
12737
|
+
workspacePort: service.workspacePort,
|
|
12738
|
+
protocol: service.protocol,
|
|
12739
|
+
hostPort: service.hostPort,
|
|
12740
|
+
authority: service.authority,
|
|
12741
|
+
browserUrl: service.browserUrl
|
|
12742
|
+
}))
|
|
12743
|
+
});
|
|
12744
|
+
return;
|
|
12745
|
+
}
|
|
11517
12746
|
if (services.length === 0) {
|
|
11518
12747
|
say7(dim7("no live services \u2014 mend service add <port> adopts a listening one"));
|
|
11519
12748
|
return;
|
|
@@ -11552,48 +12781,184 @@ var autoConnect = async (config, service) => {
|
|
|
11552
12781
|
say7(dim7(" remote server \u2014 tunneling the port here \xB7 Ctrl-C stops the tunnel, not the Service"));
|
|
11553
12782
|
await tunnelServices(config, [service], null);
|
|
11554
12783
|
};
|
|
11555
|
-
var
|
|
12784
|
+
var SERVICE_WAIT_DEFAULT_MS = (() => {
|
|
11556
12785
|
const configured = Number(process.env["MEND_SERVICE_WAIT_MS"]);
|
|
11557
|
-
return Number.isFinite(configured) && configured > 0 ? configured :
|
|
12786
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 10 * 6e4;
|
|
11558
12787
|
})();
|
|
11559
|
-
var
|
|
11560
|
-
|
|
12788
|
+
var SERVICE_RUN_VALUED = /* @__PURE__ */ new Set(["--port", "--name", "--timeout"]);
|
|
12789
|
+
var serviceWaitOf = (head) => {
|
|
12790
|
+
const timeoutFlag = head.indexOf("--timeout");
|
|
12791
|
+
const timeoutText = timeoutFlag === -1 ? void 0 : head[timeoutFlag + 1];
|
|
12792
|
+
if (!head.includes("--wait")) {
|
|
12793
|
+
return timeoutFlag === -1 ? null : fail7("--timeout bounds --wait: pass --wait too, or leave --timeout out");
|
|
12794
|
+
}
|
|
12795
|
+
const timeoutMs = timeoutFlag === -1 ? SERVICE_WAIT_DEFAULT_MS : timeoutText === void 0 ? null : parseDuration(timeoutText);
|
|
12796
|
+
if (timeoutMs === null) return fail7(`${DURATION_ISSUE} \xB7 ${usageOf("service run")}`);
|
|
12797
|
+
return { deadline: clock.now() + timeoutMs, timeoutMs };
|
|
12798
|
+
};
|
|
12799
|
+
var serviceRunPositionals = (head) => head.filter(
|
|
12800
|
+
(arg, index) => !arg.startsWith("--") && !SERVICE_RUN_VALUED.has(head[index - 1] ?? "")
|
|
12801
|
+
);
|
|
12802
|
+
var failWith = (code, message) => {
|
|
12803
|
+
process.stderr.write(`mend: ${redactCredentials(message)}
|
|
12804
|
+
`);
|
|
12805
|
+
return exitFlushed(code);
|
|
12806
|
+
};
|
|
12807
|
+
var withinServiceWait = async (work, wait) => {
|
|
12808
|
+
const bounded = await beforeDeadline(work, clock, wait?.deadline ?? null);
|
|
11561
12809
|
if (bounded.done) return bounded.value;
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
`
|
|
12810
|
+
return failWith(
|
|
12811
|
+
WAIT_TIMED_OUT,
|
|
12812
|
+
`Mend gave no answer within ${durationLine(wait?.timeoutMs ?? 0)} \xB7 nothing was started`
|
|
12813
|
+
);
|
|
12814
|
+
};
|
|
12815
|
+
var errorText2 = (error) => error instanceof Error ? error.message : String(error);
|
|
12816
|
+
var exitCodeText = (exitCode) => exitCode === null ? "no exit code reported" : `code ${exitCode}`;
|
|
12817
|
+
var readServiceStart = async (config, sessionId) => {
|
|
12818
|
+
const [services, sessionStatus] = await Promise.all([
|
|
12819
|
+
request(config, "GET", "/services?all=1"),
|
|
12820
|
+
request(config, "GET", `/sessions/${sessionId}`).then(
|
|
12821
|
+
(detail) => detail.session.status,
|
|
12822
|
+
(error) => {
|
|
12823
|
+
if (error instanceof MendRequestError && error.kind === "http" && error.status === 404) {
|
|
12824
|
+
return null;
|
|
12825
|
+
}
|
|
12826
|
+
throw error;
|
|
12827
|
+
}
|
|
12828
|
+
)
|
|
12829
|
+
]);
|
|
12830
|
+
return { services, sessionStatus };
|
|
12831
|
+
};
|
|
12832
|
+
var reportAnswered = (config, service) => {
|
|
12833
|
+
const name = service.label;
|
|
12834
|
+
say7(`${green7("\u2713")} Service ${name} \xB7 ${service.status}`);
|
|
12835
|
+
printServiceEndpoint(config, service, false);
|
|
12836
|
+
say7(dim7(` logs: mend service logs ${name}`));
|
|
12837
|
+
if (willAutoConnect(config, service, false)) say7(dim7(` connect: mend service connect ${name}`));
|
|
12838
|
+
};
|
|
12839
|
+
var startServiceWaited = async (config, session, target, wait, spinner, start) => {
|
|
12840
|
+
const { name, port } = target;
|
|
12841
|
+
const startId = crypto.randomUUID();
|
|
12842
|
+
const after = durationLine(wait.timeoutMs);
|
|
12843
|
+
const stillStarting = (processId, more = "") => failWith(
|
|
12844
|
+
WAIT_TIMED_OUT,
|
|
12845
|
+
`${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`
|
|
12846
|
+
);
|
|
12847
|
+
const ended = (processId, status, exitCode, more) => failWith(
|
|
12848
|
+
SERVICE_PROCESS_ENDED,
|
|
12849
|
+
`${name} \xB7 process ${processId.slice(0, 8)} ${status} \xB7 ${exitCodeText(exitCode)} \xB7 before :${port} answered \xB7 ${more}`
|
|
12850
|
+
);
|
|
12851
|
+
const posted = await withSpinner(
|
|
12852
|
+
spinner,
|
|
12853
|
+
beforeDeadline(
|
|
12854
|
+
start(startId).then(
|
|
12855
|
+
(view) => ({ ok: true, view }),
|
|
12856
|
+
(error) => ({ ok: false, error })
|
|
12857
|
+
),
|
|
12858
|
+
clock,
|
|
12859
|
+
wait.deadline
|
|
12860
|
+
)
|
|
11565
12861
|
);
|
|
11566
|
-
return
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
12862
|
+
if (!posted.done) return stillStarting(null, " \xB7 the start request has not returned");
|
|
12863
|
+
let attemptId = null;
|
|
12864
|
+
if (posted.value.ok) {
|
|
12865
|
+
const { view } = posted.value;
|
|
12866
|
+
attemptId = answeredAttemptId(view, startId);
|
|
12867
|
+
if (attemptId !== null) {
|
|
12868
|
+
const first = serviceStartStateOf(
|
|
12869
|
+
{ services: [view], sessionStatus: session.status },
|
|
12870
|
+
{ attemptId, sessionId: session.id, startId }
|
|
12871
|
+
);
|
|
12872
|
+
if (first.kind === "answered") return reportAnswered(config, flattenService(view));
|
|
12873
|
+
if (first.kind === "process-ended") {
|
|
12874
|
+
return ended(first.processId, first.status, first.exitCode, `mend logs --service ${name}`);
|
|
12875
|
+
}
|
|
12876
|
+
}
|
|
12877
|
+
say7(
|
|
12878
|
+
`${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`
|
|
11574
12879
|
);
|
|
11575
|
-
}
|
|
11576
|
-
|
|
11577
|
-
|
|
12880
|
+
} else {
|
|
12881
|
+
const { error } = posted.value;
|
|
12882
|
+
if (!mayStillBeWorking(error)) {
|
|
12883
|
+
const read = await readWithin(
|
|
12884
|
+
() => request(config, "GET", "/services?all=1"),
|
|
12885
|
+
clock,
|
|
12886
|
+
wait.deadline
|
|
12887
|
+
).catch(() => null);
|
|
12888
|
+
const found = read?.done === true ? findStartAttempt(read.value, { attemptId: null, sessionId: session.id, startId }) : void 0;
|
|
12889
|
+
if (found !== void 0 && found.attempt.exitedAt !== null) {
|
|
12890
|
+
return ended(
|
|
12891
|
+
found.attempt.id,
|
|
12892
|
+
found.attempt.status,
|
|
12893
|
+
found.attempt.exitCode,
|
|
12894
|
+
errorText2(error)
|
|
12895
|
+
);
|
|
12896
|
+
}
|
|
12897
|
+
return failWith(1, errorText2(error));
|
|
12898
|
+
}
|
|
12899
|
+
say7(dim7(` the start got no answer (${errorText2(error)}) \xB7 looking for the attempt it began\u2026`));
|
|
12900
|
+
}
|
|
12901
|
+
const started = clock.now();
|
|
12902
|
+
let line = `${name} \xB7 starting \xB7 waiting for :${port} to answer\u2026`;
|
|
12903
|
+
let nextReport = started + 6e4;
|
|
12904
|
+
const outcome = await withSpinner(
|
|
12905
|
+
() => line,
|
|
12906
|
+
waitForServiceStart({
|
|
12907
|
+
read: () => readServiceStart(config, session.id),
|
|
12908
|
+
target: { attemptId, sessionId: session.id, startId },
|
|
12909
|
+
deadline: wait.deadline,
|
|
12910
|
+
...clock,
|
|
12911
|
+
onStarting: (state) => {
|
|
12912
|
+
const running = state.processId === null ? "" : ` \xB7 process ${state.processId.slice(0, 8)} runs`;
|
|
12913
|
+
line = `${name} \xB7 starting${running} \xB7 :${port} has not answered`;
|
|
12914
|
+
if (chrome.isTTY !== true && clock.now() >= nextReport) {
|
|
12915
|
+
nextReport = clock.now() + 6e4;
|
|
12916
|
+
say7(dim7(` ${line} \xB7 ${durationLine(clock.now() - started)} so far`));
|
|
12917
|
+
}
|
|
12918
|
+
}
|
|
12919
|
+
})
|
|
12920
|
+
).catch((error) => failFlushed(errorText2(error)));
|
|
12921
|
+
switch (outcome.kind) {
|
|
12922
|
+
case "answered":
|
|
12923
|
+
return reportAnswered(config, flattenService(outcome.view));
|
|
12924
|
+
case "process-ended":
|
|
12925
|
+
return ended(
|
|
12926
|
+
outcome.processId,
|
|
12927
|
+
outcome.status,
|
|
12928
|
+
outcome.exitCode,
|
|
12929
|
+
`mend logs --service ${name}`
|
|
12930
|
+
);
|
|
12931
|
+
case "workspace-ended":
|
|
12932
|
+
return failWith(
|
|
12933
|
+
SERVICE_WORKSPACE_ENDED,
|
|
12934
|
+
`${name} \xB7 session ${session.id.slice(0, 8)} no longer exists, and its workspace went with it \xB7 before :${port} answered`
|
|
12935
|
+
);
|
|
12936
|
+
case "timeout":
|
|
12937
|
+
return stillStarting(outcome.last?.kind === "starting" ? outcome.last.processId : null);
|
|
12938
|
+
case "no-attempt":
|
|
12939
|
+
return failWith(
|
|
12940
|
+
1,
|
|
12941
|
+
`${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`
|
|
12942
|
+
);
|
|
11578
12943
|
}
|
|
11579
12944
|
};
|
|
11580
12945
|
var serviceRun = async (config, args) => {
|
|
11581
12946
|
const dashdash = args.indexOf("--");
|
|
11582
12947
|
const usage2 = usageOf("service run");
|
|
11583
|
-
const
|
|
11584
|
-
const
|
|
12948
|
+
const head = dashdash === -1 ? args : args.slice(0, dashdash);
|
|
12949
|
+
const wait = serviceWaitOf(head);
|
|
11585
12950
|
if (dashdash === -1) {
|
|
11586
|
-
const positionals =
|
|
12951
|
+
const positionals = serviceRunPositionals(head);
|
|
11587
12952
|
const name2 = positionals.at(-1);
|
|
11588
12953
|
if (name2 === void 0) return fail7(usage2);
|
|
11589
12954
|
const prefix2 = positionals.length > 1 ? positionals[0] : void 0;
|
|
11590
12955
|
const session2 = await withinServiceWait(
|
|
11591
12956
|
resolveLiveSession(config, prefix2, "service run"),
|
|
11592
|
-
|
|
12957
|
+
wait
|
|
11593
12958
|
);
|
|
11594
12959
|
const recipes = await withinServiceWait(
|
|
11595
12960
|
api(config, "GET", `/sessions/${session2.id}/recipes`),
|
|
11596
|
-
|
|
12961
|
+
wait
|
|
11597
12962
|
);
|
|
11598
12963
|
const recipe = recipes.find((entry) => entry.name === name2);
|
|
11599
12964
|
if (recipe === void 0) {
|
|
@@ -11602,34 +12967,43 @@ var serviceRun = async (config, args) => {
|
|
|
11602
12967
|
recipes.length === 0 ? `no mend.toml recipes in this worktree \u2014 declare [service.${name2}] first` : `no recipe named "${name2}" \u2014 declared: ${known}`
|
|
11603
12968
|
);
|
|
11604
12969
|
}
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
12970
|
+
const startRecipe = (startId) => request(config, "POST", `/sessions/${session2.id}/services/recipe`, {
|
|
12971
|
+
name: recipe.name,
|
|
12972
|
+
startId
|
|
12973
|
+
});
|
|
12974
|
+
if (wait !== null) {
|
|
12975
|
+
if (recipe.protocol === "udp") {
|
|
12976
|
+
return fail7(`--wait needs a TCP port: ${recipe.name} is UDP, which has no probe`);
|
|
12977
|
+
}
|
|
12978
|
+
if (recipe.command === null) {
|
|
12979
|
+
return fail7(
|
|
12980
|
+
`--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`
|
|
12981
|
+
);
|
|
12982
|
+
}
|
|
12983
|
+
return startServiceWaited(
|
|
12984
|
+
config,
|
|
12985
|
+
session2,
|
|
12986
|
+
{ name: recipe.name, port: recipe.port },
|
|
12987
|
+
wait,
|
|
12988
|
+
`starting ${recipe.name} \u2014 waiting for :${recipe.port} to answer\u2026`,
|
|
12989
|
+
startRecipe
|
|
11611
12990
|
);
|
|
11612
12991
|
}
|
|
11613
12992
|
const service2 = await withSpinner(
|
|
11614
12993
|
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`,
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
}),
|
|
11619
|
-
deadline
|
|
11620
|
-
)
|
|
12994
|
+
mutateService(config, "POST", `/sessions/${session2.id}/services/recipe`, {
|
|
12995
|
+
name: recipe.name
|
|
12996
|
+
})
|
|
11621
12997
|
);
|
|
11622
|
-
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect")
|
|
12998
|
+
const tunneling2 = willAutoConnect(config, service2, args.includes("--no-connect"));
|
|
11623
12999
|
say7(`${green7("\u2713")} Service ${service2.label ?? ""} \xB7 ${service2.status}`);
|
|
11624
13000
|
printServiceEndpoint(config, service2, tunneling2);
|
|
11625
13001
|
say7(dim7(` logs: mend service logs ${service2.label ?? service2.id.slice(0, 8)}`));
|
|
11626
|
-
failUnlessAnswered(config, service2, wait);
|
|
11627
13002
|
if (tunneling2) await autoConnect(config, service2);
|
|
11628
13003
|
return;
|
|
11629
13004
|
}
|
|
11630
13005
|
const argv = args.slice(dashdash + 1);
|
|
11631
13006
|
if (argv.length === 0) return fail7(usage2);
|
|
11632
|
-
const head = args.slice(0, dashdash);
|
|
11633
13007
|
const portFlag = head.indexOf("--port");
|
|
11634
13008
|
const port = portFlag === -1 ? Number.NaN : Number(head[portFlag + 1]);
|
|
11635
13009
|
if (!Number.isInteger(port) || port < 1 || port > 65535) return fail7(usage2);
|
|
@@ -11641,34 +13015,33 @@ var serviceRun = async (config, args) => {
|
|
|
11641
13015
|
if (http && https) return fail7(usage2);
|
|
11642
13016
|
const browserScheme = https ? "https" : http ? "http" : null;
|
|
11643
13017
|
if (protocol === "udp" && browserScheme !== null) return fail7(usage2);
|
|
11644
|
-
if (protocol === "udp" && wait) {
|
|
13018
|
+
if (protocol === "udp" && wait !== null) {
|
|
11645
13019
|
return fail7("--wait needs a TCP port: UDP has no probe, so nothing could be waited for");
|
|
11646
13020
|
}
|
|
11647
|
-
const prefix = head
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
13021
|
+
const prefix = serviceRunPositionals(head)[0];
|
|
13022
|
+
const session = await withinServiceWait(resolveLiveSession(config, prefix, "service run"), wait);
|
|
13023
|
+
const body = { argv, port, name, protocol, browserScheme };
|
|
13024
|
+
if (wait !== null) {
|
|
13025
|
+
return startServiceWaited(
|
|
13026
|
+
config,
|
|
13027
|
+
session,
|
|
13028
|
+
{ name: name ?? argv[0] ?? "service", port },
|
|
13029
|
+
wait,
|
|
13030
|
+
`starting ${name ?? argv[0]} \u2014 waiting for :${port} to answer\u2026`,
|
|
13031
|
+
(startId) => request(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
13032
|
+
...body,
|
|
13033
|
+
startId
|
|
13034
|
+
})
|
|
13035
|
+
);
|
|
13036
|
+
}
|
|
11654
13037
|
const service = await withSpinner(
|
|
11655
13038
|
protocol === "udp" ? `starting ${name ?? argv[0]} (udp :${port})\u2026` : `starting ${name ?? argv[0]} \u2014 waiting for :${port} to answer\u2026`,
|
|
11656
|
-
|
|
11657
|
-
mutateService(config, "POST", `/sessions/${session.id}/services/run`, {
|
|
11658
|
-
argv,
|
|
11659
|
-
port,
|
|
11660
|
-
name,
|
|
11661
|
-
protocol,
|
|
11662
|
-
browserScheme
|
|
11663
|
-
}),
|
|
11664
|
-
deadline
|
|
11665
|
-
)
|
|
13039
|
+
mutateService(config, "POST", `/sessions/${session.id}/services/run`, body)
|
|
11666
13040
|
);
|
|
11667
|
-
const tunneling = willAutoConnect(config, service, head.includes("--no-connect")
|
|
13041
|
+
const tunneling = willAutoConnect(config, service, head.includes("--no-connect"));
|
|
11668
13042
|
say7(`${green7("\u2713")} Service ${service.label ?? ""} \xB7 ${service.status}`);
|
|
11669
13043
|
printServiceEndpoint(config, service, tunneling);
|
|
11670
13044
|
say7(dim7(` logs: mend service logs ${service.label ?? service.id.slice(0, 8)}`));
|
|
11671
|
-
failUnlessAnswered(config, service, wait);
|
|
11672
13045
|
if (tunneling) await autoConnect(config, service);
|
|
11673
13046
|
};
|
|
11674
13047
|
var serviceLogs = async (config, args) => {
|
|
@@ -11740,12 +13113,12 @@ var serviceRestart = async (config, args) => {
|
|
|
11740
13113
|
var serviceInit = async (args) => {
|
|
11741
13114
|
const root = gitTopLevel(process.cwd()) ?? process.cwd();
|
|
11742
13115
|
const target = path21.join(root, "mend.toml");
|
|
11743
|
-
if (
|
|
13116
|
+
if (fs21.existsSync(target)) {
|
|
11744
13117
|
return fail7(`${target} already exists \u2014 edit it directly (init never merges)`);
|
|
11745
13118
|
}
|
|
11746
|
-
const rootFiles =
|
|
13119
|
+
const rootFiles = fs21.readdirSync(root);
|
|
11747
13120
|
const proposals = [];
|
|
11748
|
-
const readRoot = (name) =>
|
|
13121
|
+
const readRoot = (name) => fs21.readFileSync(path21.join(root, name), "utf8");
|
|
11749
13122
|
if (rootFiles.includes("package.json")) {
|
|
11750
13123
|
proposals.push(...proposeFromPackageJson(readRoot("package.json"), rootFiles));
|
|
11751
13124
|
}
|
|
@@ -11755,13 +13128,13 @@ var serviceInit = async (args) => {
|
|
|
11755
13128
|
);
|
|
11756
13129
|
for (const glob of globs) {
|
|
11757
13130
|
const parent = glob.replace(/\/?\*+$/, "");
|
|
11758
|
-
const dirs = glob.endsWith("*") ?
|
|
13131
|
+
const dirs = glob.endsWith("*") ? fs21.existsSync(path21.join(root, parent)) ? fs21.readdirSync(path21.join(root, parent)).map((dir) => path21.join(parent, dir)) : [] : [glob];
|
|
11759
13132
|
for (const dir of dirs) {
|
|
11760
13133
|
const manifest = path21.join(root, dir, "package.json");
|
|
11761
|
-
if (!
|
|
13134
|
+
if (!fs21.existsSync(manifest)) continue;
|
|
11762
13135
|
for (const proposal of proposeFromWorkspacePackage(
|
|
11763
13136
|
path21.basename(dir),
|
|
11764
|
-
|
|
13137
|
+
fs21.readFileSync(manifest, "utf8"),
|
|
11765
13138
|
rootFiles
|
|
11766
13139
|
)) {
|
|
11767
13140
|
if (!proposals.some((existing) => existing.name === proposal.name)) {
|
|
@@ -11805,7 +13178,7 @@ var serviceInit = async (args) => {
|
|
|
11805
13178
|
return;
|
|
11806
13179
|
}
|
|
11807
13180
|
}
|
|
11808
|
-
|
|
13181
|
+
fs21.writeFileSync(target, toml);
|
|
11809
13182
|
say7(`${green7("\u2713")} wrote ${target} \u2014 commit it, then: mend service run ${proposals[0]?.name}`);
|
|
11810
13183
|
};
|
|
11811
13184
|
var tunnelServices = async (config, services, portOverride) => {
|
|
@@ -11896,8 +13269,9 @@ var serviceCommand = async (config, args) => {
|
|
|
11896
13269
|
case "connect":
|
|
11897
13270
|
return serviceConnect(config, rest);
|
|
11898
13271
|
case "list":
|
|
13272
|
+
return serviceList(config, rest);
|
|
11899
13273
|
case void 0:
|
|
11900
|
-
return serviceList(config);
|
|
13274
|
+
return serviceList(config, []);
|
|
11901
13275
|
case "logs":
|
|
11902
13276
|
return serviceLogs(config, rest);
|
|
11903
13277
|
case "restart":
|
|
@@ -12034,7 +13408,7 @@ var secretsCommand = async (config, args) => {
|
|
|
12034
13408
|
const from = takeFlagValue4(rest, "--from");
|
|
12035
13409
|
let bytes;
|
|
12036
13410
|
try {
|
|
12037
|
-
bytes =
|
|
13411
|
+
bytes = fs21.readFileSync(from ?? 0);
|
|
12038
13412
|
} catch (error) {
|
|
12039
13413
|
return fail7(
|
|
12040
13414
|
from === null ? `nothing on stdin: pass --from <file> or pipe the content in` : `${from}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -12063,7 +13437,7 @@ var secretsCommand = async (config, args) => {
|
|
|
12063
13437
|
for (const line of secretFileLines(view.files)) say7(line);
|
|
12064
13438
|
};
|
|
12065
13439
|
var isProvider = (value) => value === "claude" || value === "codex" || value === "github";
|
|
12066
|
-
var readIfExists = (file) =>
|
|
13440
|
+
var readIfExists = (file) => fs21.existsSync(file) ? fs21.readFileSync(file, "utf8") : null;
|
|
12067
13441
|
var localCredential = (provider) => {
|
|
12068
13442
|
const home = os13.homedir();
|
|
12069
13443
|
switch (provider) {
|
|
@@ -12220,7 +13594,7 @@ var claudeGrant = async () => {
|
|
|
12220
13594
|
say7(dim7(" your own Claude login still works \xB7 verified"));
|
|
12221
13595
|
}
|
|
12222
13596
|
const kept = claudeGrantDir(home);
|
|
12223
|
-
if (
|
|
13597
|
+
if (fs21.existsSync(kept)) {
|
|
12224
13598
|
forgetGrant(kept);
|
|
12225
13599
|
say7(dim7(` removed the copy an older mend kept in ${kept} \xB7 the server refreshes the login`));
|
|
12226
13600
|
}
|
|
@@ -12276,7 +13650,7 @@ var connectCommand = async (config, args) => {
|
|
|
12276
13650
|
}
|
|
12277
13651
|
let secret;
|
|
12278
13652
|
if (flags.includes("--from-stdin")) {
|
|
12279
|
-
secret =
|
|
13653
|
+
secret = fs21.readFileSync(0, "utf8").trim();
|
|
12280
13654
|
if (secret === "") return fail7("nothing on stdin");
|
|
12281
13655
|
} else if (provider === "codex" && !flags.includes("--use-my-login")) {
|
|
12282
13656
|
const grant = codexGrant({
|
|
@@ -12331,14 +13705,14 @@ var takeFlagValue4 = (args, flag) => {
|
|
|
12331
13705
|
return at !== -1 && args[at + 1] !== void 0 ? String(args[at + 1]) : null;
|
|
12332
13706
|
};
|
|
12333
13707
|
var saveCliConfig = (next) => {
|
|
12334
|
-
|
|
12335
|
-
|
|
13708
|
+
fs21.mkdirSync(path21.dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
13709
|
+
fs21.writeFileSync(
|
|
12336
13710
|
CONFIG_PATH,
|
|
12337
13711
|
`${JSON.stringify({ url: next.url, token: next.token, deviceId: next.deviceId }, null, 2)}
|
|
12338
13712
|
`,
|
|
12339
13713
|
{ mode: 384 }
|
|
12340
13714
|
);
|
|
12341
|
-
|
|
13715
|
+
fs21.chmodSync(CONFIG_PATH, 384);
|
|
12342
13716
|
};
|
|
12343
13717
|
var login = async (config, args) => {
|
|
12344
13718
|
await loginCommand(args, {
|
|
@@ -12351,7 +13725,7 @@ var login = async (config, args) => {
|
|
|
12351
13725
|
});
|
|
12352
13726
|
};
|
|
12353
13727
|
var logout = async (config) => {
|
|
12354
|
-
if (!
|
|
13728
|
+
if (!fs21.existsSync(CONFIG_PATH) && config.token === null) {
|
|
12355
13729
|
say7(dim7("nothing saved \u2014 already signed out"));
|
|
12356
13730
|
return;
|
|
12357
13731
|
}
|
|
@@ -12405,7 +13779,7 @@ var uninstallCommand = async (config, args) => {
|
|
|
12405
13779
|
server,
|
|
12406
13780
|
cliHome: mendCliHome(),
|
|
12407
13781
|
sshConfigFile: path21.join(os13.homedir(), ".ssh", "config"),
|
|
12408
|
-
signedIn: config.token === null || !
|
|
13782
|
+
signedIn: config.token === null || !fs21.existsSync(CONFIG_PATH) ? null : { url: config.url, deviceId: config.deviceId },
|
|
12409
13783
|
revokeDevice: async () => {
|
|
12410
13784
|
if (config.deviceId === null) return "no device id saved";
|
|
12411
13785
|
try {
|
|
@@ -12414,7 +13788,12 @@ var uninstallCommand = async (config, args) => {
|
|
|
12414
13788
|
} catch (error) {
|
|
12415
13789
|
return error instanceof Error ? error.message : String(error);
|
|
12416
13790
|
}
|
|
12417
|
-
}
|
|
13791
|
+
},
|
|
13792
|
+
removeWorkspaceSshKey: () => removeThisMachineKey(
|
|
13793
|
+
(method, route, body) => request(config, method, route, body),
|
|
13794
|
+
mendCliHome(),
|
|
13795
|
+
config.url
|
|
13796
|
+
)
|
|
12418
13797
|
};
|
|
12419
13798
|
const plan = await describeUninstall(runtime, scope);
|
|
12420
13799
|
say7(dim7(`mend uninstall \xB7 ${scope === "all" ? "everything" : scope}`));
|
|
@@ -12546,7 +13925,7 @@ var keysMode = async (config, args) => {
|
|
|
12546
13925
|
access.bridge.connected ? `${green7("\u25CF")} signer connected \xB7 ${access.bridge.clientName ?? "unknown machine"}` : dim7(" no signer connected \u2014 mend shares the agent whenever it runs (or: mend keys share)")
|
|
12547
13926
|
);
|
|
12548
13927
|
};
|
|
12549
|
-
var
|
|
13928
|
+
var keysCommand2 = async (config, args) => {
|
|
12550
13929
|
const [verb] = args;
|
|
12551
13930
|
switch (verb) {
|
|
12552
13931
|
case "init":
|
|
@@ -12649,7 +14028,7 @@ var envLoad = async (config, args) => {
|
|
|
12649
14028
|
const file = path21.resolve(positional[0] ?? ".env");
|
|
12650
14029
|
let contents;
|
|
12651
14030
|
try {
|
|
12652
|
-
contents =
|
|
14031
|
+
contents = fs21.readFileSync(file, "utf8");
|
|
12653
14032
|
} catch {
|
|
12654
14033
|
return fail7(`cannot read ${file} \u2014 pass a path: mend env load path/to/.env`);
|
|
12655
14034
|
}
|
|
@@ -13026,11 +14405,11 @@ var pause = (ms, signal) => {
|
|
|
13026
14405
|
};
|
|
13027
14406
|
var clock = { sleep: pause, now: Date.now };
|
|
13028
14407
|
var printJson = (value) => {
|
|
13029
|
-
process.stdout.write(`${
|
|
14408
|
+
process.stdout.write(`${jsonWithoutCredentials(value)}
|
|
13030
14409
|
`);
|
|
13031
14410
|
};
|
|
13032
14411
|
var outputOnTerminal = false;
|
|
13033
|
-
var
|
|
14412
|
+
var recordedOutput = false;
|
|
13034
14413
|
var outputStopped = false;
|
|
13035
14414
|
var restoreTerminal = () => {
|
|
13036
14415
|
if (!outputOnTerminal || process.stdout.isTTY !== true) return;
|
|
@@ -13038,13 +14417,17 @@ var restoreTerminal = () => {
|
|
|
13038
14417
|
process.stdout.write(`${TERMINAL_MODES_RESET}\x1B[0m`);
|
|
13039
14418
|
};
|
|
13040
14419
|
process.once("exit", restoreTerminal);
|
|
14420
|
+
var onClosedPipe = (error) => {
|
|
14421
|
+
if (error.code !== "EPIPE") throw error;
|
|
14422
|
+
if (recordedOutput) return;
|
|
14423
|
+
process.exit(0);
|
|
14424
|
+
};
|
|
14425
|
+
process.stdout.on("error", onClosedPipe);
|
|
14426
|
+
process.stderr.on("error", onClosedPipe);
|
|
13041
14427
|
var writeOutput = (bytes) => {
|
|
13042
14428
|
if (outputStopped) return Promise.reject(new Error("stopped by a signal"));
|
|
13043
14429
|
if (process.stdout.isTTY === true) outputOnTerminal = true;
|
|
13044
|
-
|
|
13045
|
-
stdoutErrorsHandled = true;
|
|
13046
|
-
process.stdout.on("error", () => void 0);
|
|
13047
|
-
}
|
|
14430
|
+
recordedOutput = true;
|
|
13048
14431
|
return new Promise((resolve8, reject) => {
|
|
13049
14432
|
process.stdout.write(bytes, (error) => {
|
|
13050
14433
|
if (error === null || error === void 0) resolve8();
|
|
@@ -13224,26 +14607,57 @@ var resolveAnySession = async (config, word) => {
|
|
|
13224
14607
|
if ("error" in picked) return fail7(picked.error);
|
|
13225
14608
|
return picked.session;
|
|
13226
14609
|
};
|
|
14610
|
+
var serviceLogsTarget = async (config, needle, sessionWord) => {
|
|
14611
|
+
const session = sessionWord === null ? null : await resolveAnySession(config, sessionWord);
|
|
14612
|
+
const picked = pickServiceAttempt(
|
|
14613
|
+
await fetchServiceViews(config, true),
|
|
14614
|
+
needle,
|
|
14615
|
+
session?.id ?? null
|
|
14616
|
+
);
|
|
14617
|
+
if ("error" in picked) return fail7(picked.error);
|
|
14618
|
+
return { processId: picked.processId, again: `mend logs --service ${needle}` };
|
|
14619
|
+
};
|
|
13227
14620
|
var logsCommand = async (config, args) => {
|
|
13228
14621
|
chromeToStderr();
|
|
13229
14622
|
const parsed = parseLogsArgs(args);
|
|
13230
14623
|
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("logs")}`);
|
|
13231
|
-
const { session: word, follow, from, process: processPrefix } = parsed.args;
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
)
|
|
14624
|
+
const { session: word, follow, from, process: processPrefix, service } = parsed.args;
|
|
14625
|
+
let target;
|
|
14626
|
+
if (service !== null) {
|
|
14627
|
+
target = await serviceLogsTarget(config, service, word);
|
|
14628
|
+
} else {
|
|
14629
|
+
const session = word === null ? await resolveLiveSession(config, void 0, "logs") : await resolveAnySession(config, word);
|
|
14630
|
+
const id8 = session.id.slice(0, 8);
|
|
14631
|
+
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
14632
|
+
const picked = pickProcess(detail, processPrefix);
|
|
14633
|
+
if ("error" in picked) {
|
|
14634
|
+
const asService = processPrefix === null ? null : pickServiceAttempt(await fetchServiceViews(config, true), processPrefix, session.id);
|
|
14635
|
+
if (asService === null || "error" in asService && !asService.named) {
|
|
14636
|
+
return fail7(`session ${id8} \xB7 ${picked.error}`);
|
|
14637
|
+
}
|
|
14638
|
+
if ("error" in asService) return fail7(`session ${id8} \xB7 ${asService.error}`);
|
|
14639
|
+
target = {
|
|
14640
|
+
processId: asService.processId,
|
|
14641
|
+
again: `mend logs ${id8} --process ${processPrefix}`
|
|
14642
|
+
};
|
|
14643
|
+
} else {
|
|
14644
|
+
const chosen = picked.process;
|
|
14645
|
+
if (chosen.id === void 0 || chosen.sealantSessionId === null) {
|
|
14646
|
+
return fail7(
|
|
14647
|
+
`session ${id8} \xB7 this process has no recorded terminal (an adopted port, or a server older than process ids)`
|
|
14648
|
+
);
|
|
14649
|
+
}
|
|
14650
|
+
target = {
|
|
14651
|
+
processId: chosen.id,
|
|
14652
|
+
again: processPrefix === null ? `mend logs ${id8}` : `mend logs ${id8} --process ${processPrefix}`
|
|
14653
|
+
};
|
|
14654
|
+
}
|
|
13241
14655
|
}
|
|
13242
14656
|
stopOutputOnSignal(
|
|
13243
|
-
follow ? `stopped watching \xB7 the process keeps running \xB7
|
|
14657
|
+
follow ? `stopped watching \xB7 the process keeps running \xB7 ${target.again} --follow` : "stopped reading the record"
|
|
13244
14658
|
);
|
|
13245
14659
|
try {
|
|
13246
|
-
const last = await writeProcessLogs(config, target.
|
|
14660
|
+
const last = await writeProcessLogs(config, target.processId, from, follow);
|
|
13247
14661
|
restoreTerminal();
|
|
13248
14662
|
if (follow) say7(dim7(`
|
|
13249
14663
|
record ended \xB7 ${last.status} \xB7 next sequence ${last.next}`));
|
|
@@ -13260,7 +14674,7 @@ var waitCommand = async (config, args) => {
|
|
|
13260
14674
|
if ("error" in parsed) return fail7(`${parsed.error} \xB7 ${usageOf("wait")}`);
|
|
13261
14675
|
const { session: word, timeoutMs, json: json2, process: processPrefix } = parsed.args;
|
|
13262
14676
|
const deadline = timeoutMs === null ? null : clock.now() + timeoutMs;
|
|
13263
|
-
const after =
|
|
14677
|
+
const after = durationLine(timeoutMs ?? 0);
|
|
13264
14678
|
const timedOut = async (session2, processId2, last) => {
|
|
13265
14679
|
say7(
|
|
13266
14680
|
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`
|
|
@@ -13577,7 +14991,7 @@ var modelsCommand = async (config, args) => {
|
|
|
13577
14991
|
"GET",
|
|
13578
14992
|
"/harnesses/models"
|
|
13579
14993
|
);
|
|
13580
|
-
if (args.includes("--json")) return
|
|
14994
|
+
if (args.includes("--json")) return printJson(modelCatalogJson(catalogs));
|
|
13581
14995
|
for (const line of modelCatalogLines(catalogs, dim7)) say7(line);
|
|
13582
14996
|
};
|
|
13583
14997
|
var sessionsCommand = async (config, args) => {
|
|
@@ -13585,7 +14999,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
13585
14999
|
if (args.includes("--json=v2")) {
|
|
13586
15000
|
const projectFlag2 = args.indexOf("--project");
|
|
13587
15001
|
const projectName2 = projectFlag2 !== -1 && args[projectFlag2 + 1] !== void 0 ? String(args[projectFlag2 + 1]) : null;
|
|
13588
|
-
|
|
15002
|
+
printJson(await buildWorktreesJson(config, projectName2));
|
|
13589
15003
|
return;
|
|
13590
15004
|
}
|
|
13591
15005
|
const json2 = args.includes("--json");
|
|
@@ -13624,7 +15038,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
13624
15038
|
}
|
|
13625
15039
|
if (rows.length === 0) {
|
|
13626
15040
|
if (json2) {
|
|
13627
|
-
|
|
15041
|
+
printJson({ version: 1, sessions: [] });
|
|
13628
15042
|
return;
|
|
13629
15043
|
}
|
|
13630
15044
|
say7(
|
|
@@ -13670,7 +15084,7 @@ var sessionsCommand = async (config, args) => {
|
|
|
13670
15084
|
}
|
|
13671
15085
|
}))
|
|
13672
15086
|
};
|
|
13673
|
-
|
|
15087
|
+
printJson(payload);
|
|
13674
15088
|
return;
|
|
13675
15089
|
}
|
|
13676
15090
|
const workspaceLines = await workspaceLinesOf(
|
|
@@ -13766,7 +15180,7 @@ var worktreesCommand = async (config, args) => {
|
|
|
13766
15180
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
|
|
13767
15181
|
const payload = await buildWorktreesJson(config, projectName);
|
|
13768
15182
|
if (json2) {
|
|
13769
|
-
|
|
15183
|
+
printJson(payload);
|
|
13770
15184
|
return;
|
|
13771
15185
|
}
|
|
13772
15186
|
if (payload.worktrees.length === 0) {
|
|
@@ -13822,7 +15236,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
13822
15236
|
);
|
|
13823
15237
|
process.exit(rerun.status ?? 0);
|
|
13824
15238
|
}
|
|
13825
|
-
const { runDashboard } = await import("./dashboard-
|
|
15239
|
+
const { runDashboard } = await import("./dashboard-OBFWHSPD.js");
|
|
13826
15240
|
const agentShare = await startShareIfBridge(config);
|
|
13827
15241
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
13828
15242
|
listServices: () => fetchServices(config),
|
|
@@ -13897,9 +15311,9 @@ var manCommand = (words) => {
|
|
|
13897
15311
|
const version = cliVersion();
|
|
13898
15312
|
const page = doc === null ? renderManIndex(version) : renderManPage(doc, version);
|
|
13899
15313
|
const file = path21.join(os13.tmpdir(), `mend-man-${process.pid}-${manFileName(doc)}`);
|
|
13900
|
-
|
|
15314
|
+
fs21.writeFileSync(file, page);
|
|
13901
15315
|
const result = spawnSync6("man", ["-l", file], { stdio: "inherit" });
|
|
13902
|
-
|
|
15316
|
+
fs21.rmSync(file, { force: true });
|
|
13903
15317
|
if (result.error !== void 0 || result.status !== 0) {
|
|
13904
15318
|
say7(doc === null ? renderIndex() : renderCommand(doc));
|
|
13905
15319
|
}
|
|
@@ -13945,7 +15359,7 @@ var main = async () => {
|
|
|
13945
15359
|
case "uninstall":
|
|
13946
15360
|
return uninstallCommand(config, rest);
|
|
13947
15361
|
case "keys":
|
|
13948
|
-
return
|
|
15362
|
+
return keysCommand2(config, rest);
|
|
13949
15363
|
case "git-author":
|
|
13950
15364
|
return gitAuthorCommand(config, rest);
|
|
13951
15365
|
case "dotfiles":
|