openship 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +104 -61
  2. package/dist/bare-ITM45QBX.js +18 -0
  3. package/dist/bowser-MFQA6OS4.js +2821 -0
  4. package/dist/chunk-3U4SPZD5.js +241 -0
  5. package/dist/chunk-46YJFP4S.js +1771 -0
  6. package/dist/chunk-4CQWT2WL.js +116 -0
  7. package/dist/chunk-4FABQGQP.js +23 -0
  8. package/dist/chunk-4LEL3H2Z.js +84 -0
  9. package/dist/chunk-4MG3BVHE.js +326 -0
  10. package/dist/chunk-4VNS5WPM.js +42 -0
  11. package/dist/chunk-5N7VUGL6.js +2094 -0
  12. package/dist/chunk-5ZCKQ2MI.js +1360 -0
  13. package/dist/chunk-6ERIMTBC.js +1412 -0
  14. package/dist/chunk-7UOB5GGQ.js +132 -0
  15. package/dist/chunk-BUBSSKO6.js +1384 -0
  16. package/dist/chunk-E6W2JMMU.js +341 -0
  17. package/dist/chunk-E77LJWJK.js +1535 -0
  18. package/dist/chunk-F4DFVG6Q.js +573 -0
  19. package/dist/chunk-HFYDYK4D.js +1092 -0
  20. package/dist/chunk-JR2OCMJA.js +46 -0
  21. package/dist/chunk-JUJBBF5U.js +746 -0
  22. package/dist/chunk-JZLNHGLW.js +19145 -0
  23. package/dist/chunk-KD7VO4LG.js +2617 -0
  24. package/dist/chunk-LBFH7THC.js +435 -0
  25. package/dist/chunk-NB2VEIWG.js +129 -0
  26. package/dist/chunk-OJE6YWPQ.js +2866 -0
  27. package/dist/chunk-QKFRZNKN.js +4941 -0
  28. package/dist/chunk-RBZPFTHV.js +13354 -0
  29. package/dist/chunk-S6DSRLNE.js +121 -0
  30. package/dist/chunk-TEYJYSMT.js +409 -0
  31. package/dist/chunk-U2DPZSXA.js +112 -0
  32. package/dist/chunk-WJPCBU7Z.js +3147 -0
  33. package/dist/chunk-XIFRMKYT.js +13 -0
  34. package/dist/chunk-XVSAUO2I.js +7946 -0
  35. package/dist/chunk-Y37KHU7L.js +31 -0
  36. package/dist/chunk-YL67EWYZ.js +416 -0
  37. package/dist/chunk-ZKJOFWBJ.js +1879 -0
  38. package/dist/cloud-JMPHO3CW.js +7 -0
  39. package/dist/cloud-W5G6YXAY.js +24 -0
  40. package/dist/dist-L3MPOWCA.js +65 -0
  41. package/dist/dist-es-4C322O5D.js +22 -0
  42. package/dist/dist-es-GTAMF674.js +87 -0
  43. package/dist/dist-es-KE56MWYO.js +169 -0
  44. package/dist/dist-es-NRZIQY5B.js +484 -0
  45. package/dist/dist-es-WZCR7DHQ.js +317 -0
  46. package/dist/dist-es-XQCOTGC7.js +376 -0
  47. package/dist/dist-es-ZW3HFQUM.js +69 -0
  48. package/dist/docker-3FVJMFC3.js +12 -0
  49. package/dist/docker-edge-executor-ZIWAZWPL.js +9 -0
  50. package/dist/edge-import-53M5DGWZ.js +39 -0
  51. package/dist/event-streams-7LGDL56D.js +42 -0
  52. package/dist/executor-J7BHW5HF.js +26 -0
  53. package/dist/index.js +2459 -3461
  54. package/dist/loadSso-N273C5I7.js +577 -0
  55. package/dist/nginx-YZWKYKQU.js +16 -0
  56. package/dist/noop-RRNCIUI5.js +7 -0
  57. package/dist/openresty-lua-HAO4EUDY.js +32 -0
  58. package/dist/server/index.js +217916 -205055
  59. package/dist/server/migrations/0057_complete_prism.sql +18 -0
  60. package/dist/server/migrations/0058_add_server_is_local.sql +1 -0
  61. package/dist/server/migrations/0059_legal_fixer.sql +18 -0
  62. package/dist/server/migrations/0060_add_route_strategy_host_port.sql +3 -0
  63. package/dist/server/migrations/0061_notification_default_multi_kind.sql +3 -0
  64. package/dist/server/migrations/0062_add_incoming_webhook.sql +21 -0
  65. package/dist/server/migrations/0063_add_migration_logs.sql +1 -0
  66. package/dist/server/migrations/0064_add_migration_partial.sql +2 -0
  67. package/dist/server/migrations/0065_custom_app_template.sql +14 -0
  68. package/dist/server/migrations/0066_ensure_migration_partial_columns.sql +2 -0
  69. package/dist/server/migrations/0067_webhook_delivery.sql +28 -0
  70. package/dist/server/migrations/0068_add_migration_target_volumes.sql +1 -0
  71. package/dist/server/migrations/0069_add_project_composite_routes.sql +1 -0
  72. package/dist/server/migrations/0070_forward_git_to_server.sql +1 -0
  73. package/dist/server/migrations/meta/0057_snapshot.json +9326 -0
  74. package/dist/server/migrations/meta/0058_snapshot.json +9333 -0
  75. package/dist/server/migrations/meta/0059_snapshot.json +9495 -0
  76. package/dist/server/migrations/meta/0060_snapshot.json +9515 -0
  77. package/dist/server/migrations/meta/_journal.json +99 -1
  78. package/dist/setup-5X3WEEHA.js +14 -0
  79. package/dist/signin-DR462ERO.js +663 -0
  80. package/dist/sso-oidc-VBZLJUK5.js +830 -0
  81. package/dist/sts-EJLZ4Z77.js +1422 -0
  82. package/package.json +6 -2
@@ -0,0 +1,1771 @@
1
+ import {
2
+ describeProcess,
3
+ probeListeningPort,
4
+ resolveEnvironment
5
+ } from "./chunk-3U4SPZD5.js";
6
+ import {
7
+ formatDuration,
8
+ isRemoteConnectionError,
9
+ systemDebug
10
+ } from "./chunk-4LEL3H2Z.js";
11
+ import {
12
+ sq
13
+ } from "./chunk-4CQWT2WL.js";
14
+ import {
15
+ OPENRESTY_LUA_DIR,
16
+ buildReloadCommand,
17
+ deployLuaScripts,
18
+ detectOpenRestyPaths,
19
+ ensureOpenRestyConfig
20
+ } from "./chunk-E6W2JMMU.js";
21
+ import {
22
+ AppError,
23
+ compareSemver,
24
+ safeErrorMessage
25
+ } from "./chunk-JZLNHGLW.js";
26
+
27
+ // ../../packages/adapters/src/system/catalog.ts
28
+ function dockerInstallPlan(profile) {
29
+ if (profile.os !== "linux") {
30
+ return {
31
+ supported: false,
32
+ unsupportedReason: "Docker installation is only supported on Linux servers"
33
+ };
34
+ }
35
+ return {
36
+ supported: true,
37
+ installCommand: "curl -fsSL https://get.docker.com | sh",
38
+ startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
39
+ verifyCommand: "docker --version"
40
+ };
41
+ }
42
+ function gitInstallPlan(profile) {
43
+ const commands = {
44
+ apt: "apt-get update -qq && apt-get install -y -qq git",
45
+ dnf: "dnf install -y git",
46
+ yum: "yum install -y git",
47
+ apk: "apk add --no-cache git",
48
+ brew: "brew install git"
49
+ };
50
+ const installCommand = commands[profile.packageManager];
51
+ if (!installCommand) {
52
+ return {
53
+ supported: false,
54
+ unsupportedReason: "No supported package manager found for Git installation"
55
+ };
56
+ }
57
+ return {
58
+ supported: true,
59
+ installCommand,
60
+ verifyCommand: "git --version"
61
+ };
62
+ }
63
+ function rsyncInstallPlan(profile) {
64
+ const commands = {
65
+ apt: "apt-get update -qq && apt-get install -y -qq rsync",
66
+ dnf: "dnf install -y rsync",
67
+ yum: "yum install -y rsync",
68
+ apk: "apk add --no-cache rsync",
69
+ brew: "brew install rsync"
70
+ };
71
+ const installCommand = commands[profile.packageManager];
72
+ if (!installCommand) {
73
+ return {
74
+ supported: false,
75
+ unsupportedReason: "No supported package manager found for rsync installation"
76
+ };
77
+ }
78
+ return {
79
+ supported: true,
80
+ installCommand,
81
+ verifyCommand: "rsync --version | head -n 1"
82
+ };
83
+ }
84
+ var OPENRESTY_APT_SOURCES = [
85
+ // ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
86
+ // (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
87
+ // aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
88
+ // the whole setup (issue #109). Switch the base URL by arch and tag the deb
89
+ // line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
90
+ // returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
91
+ 'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
92
+ 'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
93
+ // `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
94
+ // /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
95
+ // the empty-var fallback below can pick a default.
96
+ 'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
97
+ 'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
98
+ 'OR_CODENAME=""',
99
+ "for c in $REPO_CODENAME $OR_FALLBACKS; do",
100
+ ' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
101
+ "done",
102
+ 'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
103
+ '[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
104
+ 'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
105
+ ];
106
+ function openrestyInstallPlan(profile) {
107
+ if (profile.os !== "linux") {
108
+ return {
109
+ supported: false,
110
+ unsupportedReason: "OpenResty installation is only supported on Linux servers"
111
+ };
112
+ }
113
+ let installCommand;
114
+ if (profile.packageManager === "apt") {
115
+ const distro = profile.distro === "debian" ? "debian" : "ubuntu";
116
+ installCommand = [
117
+ "set -e",
118
+ // Heal a stale/bad openresty.list left by a prior run (e.g. the #86
119
+ // 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
120
+ // set -e aborts here and the repo never gets rewritten.
121
+ "rm -f /etc/apt/sources.list.d/openresty.list",
122
+ "apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
123
+ "wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
124
+ "gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
125
+ `REPO=${distro}`,
126
+ ...OPENRESTY_APT_SOURCES,
127
+ "apt-get update -qq && apt-get install -y -qq openresty"
128
+ ].join("\n");
129
+ } else if (profile.packageManager === "dnf") {
130
+ const distro = profile.distro === "fedora" ? "fedora" : "centos";
131
+ installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
132
+ } else if (profile.packageManager === "yum") {
133
+ installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
134
+ } else if (profile.packageManager === "apk") {
135
+ installCommand = [
136
+ "apk add --no-cache wget",
137
+ "wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
138
+ `. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
139
+ "apk update && apk add openresty"
140
+ ].join(" && ");
141
+ } else {
142
+ installCommand = [
143
+ "set -e",
144
+ "if command -v apt-get >/dev/null 2>&1; then",
145
+ " rm -f /etc/apt/sources.list.d/openresty.list",
146
+ " apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
147
+ " wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
148
+ " gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
149
+ ' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
150
+ ' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
151
+ ...OPENRESTY_APT_SOURCES.map((l) => " " + l),
152
+ " apt-get update -qq && apt-get install -y -qq openresty",
153
+ "elif command -v dnf >/dev/null 2>&1; then",
154
+ " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
155
+ "elif command -v yum >/dev/null 2>&1; then",
156
+ " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
157
+ "elif command -v apk >/dev/null 2>&1; then",
158
+ " apk add --no-cache wget \\",
159
+ " && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
160
+ ' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
161
+ " && apk update && apk add openresty",
162
+ "else",
163
+ ' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
164
+ "fi"
165
+ ].join("\n");
166
+ }
167
+ return {
168
+ supported: true,
169
+ installCommand,
170
+ startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
171
+ verifyCommand: "openresty -v 2>&1"
172
+ };
173
+ }
174
+ function certbotInstallPlan(profile) {
175
+ const commands = {
176
+ apt: "apt-get update -qq && apt-get install -y -qq certbot",
177
+ dnf: "dnf install -y certbot",
178
+ yum: "yum install -y certbot",
179
+ apk: "apk add --no-cache certbot"
180
+ };
181
+ const installCommand = commands[profile.packageManager];
182
+ if (!installCommand) {
183
+ return {
184
+ supported: false,
185
+ unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
186
+ };
187
+ }
188
+ return {
189
+ supported: true,
190
+ installCommand,
191
+ verifyCommand: "certbot --version 2>/dev/null"
192
+ };
193
+ }
194
+ var systemCatalog = {
195
+ checks: {
196
+ docker: {
197
+ versionCommand: "docker --version",
198
+ daemonCommand: "docker info --format '{{.ServerVersion}}'",
199
+ parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
200
+ missingMessage: "Docker is not installed",
201
+ notRunningMessage: "Docker is installed but the daemon is not running"
202
+ },
203
+ openresty: {
204
+ versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
205
+ runningCommands: [
206
+ "pgrep -f 'nginx.*openresty' || pgrep -f '/usr/local/openresty'"
207
+ ],
208
+ parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
209
+ missingMessage: "OpenResty is not installed",
210
+ notRunningMessage: "OpenResty is installed but not running"
211
+ },
212
+ certbot: {
213
+ versionCommand: "certbot --version 2>/dev/null",
214
+ parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
215
+ missingMessage: "Certbot is not installed"
216
+ },
217
+ git: {
218
+ versionCommand: "git --version",
219
+ parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
220
+ missingMessage: "Git is not installed"
221
+ },
222
+ rsync: {
223
+ versionCommand: "rsync --version | head -n 1",
224
+ parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
225
+ missingMessage: "rsync is not installed"
226
+ }
227
+ },
228
+ installs: {
229
+ docker: dockerInstallPlan,
230
+ git: gitInstallPlan,
231
+ rsync: rsyncInstallPlan,
232
+ openresty: openrestyInstallPlan,
233
+ certbot: certbotInstallPlan
234
+ }
235
+ };
236
+
237
+ // ../../packages/adapters/src/system/available-version.ts
238
+ var PACKAGE_NAMES = {
239
+ git: "git",
240
+ rsync: "rsync",
241
+ openresty: "openresty",
242
+ certbot: "certbot",
243
+ docker: null
244
+ };
245
+ function normalizePkgVersion(raw) {
246
+ let v = raw.trim();
247
+ const colon = v.lastIndexOf(":");
248
+ if (colon !== -1) v = v.slice(colon + 1);
249
+ const dash = v.indexOf("-");
250
+ if (dash !== -1) v = v.slice(0, dash);
251
+ return v.trim();
252
+ }
253
+ async function tryExec(executor, cmd) {
254
+ try {
255
+ return await executor.exec(cmd);
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ async function probeCandidate(executor, pm, pkg) {
261
+ const q = `'${pkg.replace(/'/g, "'\\''")}'`;
262
+ if (pm === "apt") {
263
+ const out = await tryExec(executor, `apt-cache policy ${q} 2>/dev/null`);
264
+ return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
265
+ }
266
+ if (pm === "dnf" || pm === "yum") {
267
+ const out = await tryExec(
268
+ executor,
269
+ `${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
270
+ );
271
+ return out?.trim().split(/\s+/)[1]?.trim() ?? null;
272
+ }
273
+ if (pm === "apk") {
274
+ const out = await tryExec(executor, `apk policy ${q} 2>/dev/null`);
275
+ const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
276
+ return versions.length ? versions[versions.length - 1] : null;
277
+ }
278
+ return null;
279
+ }
280
+ async function enrichAvailableVersions(executor, profile, statuses) {
281
+ const targets = statuses.filter(
282
+ (s) => s.installed && s.version && PACKAGE_NAMES[s.name]
283
+ );
284
+ await Promise.all(
285
+ targets.map(async (s) => {
286
+ try {
287
+ const pkg = PACKAGE_NAMES[s.name];
288
+ const candidate = await probeCandidate(executor, profile.packageManager, pkg);
289
+ if (!candidate) return;
290
+ const avail = normalizePkgVersion(candidate);
291
+ const installed = normalizePkgVersion(s.version);
292
+ if (avail && compareSemver(avail, installed) > 0) {
293
+ s.availableVersion = avail;
294
+ s.updateAvailable = true;
295
+ }
296
+ } catch {
297
+ }
298
+ })
299
+ );
300
+ }
301
+
302
+ // ../../packages/adapters/src/system/components.ts
303
+ var SYSTEM_COMPONENTS = [
304
+ {
305
+ name: "docker",
306
+ label: "Docker",
307
+ description: "Container runtime for deployments",
308
+ installable: true,
309
+ category: "core"
310
+ },
311
+ {
312
+ name: "git",
313
+ label: "Git",
314
+ description: "Version control for source code",
315
+ installable: true,
316
+ category: "core"
317
+ },
318
+ {
319
+ name: "openresty",
320
+ label: "OpenResty",
321
+ description: "Reverse proxy with Lua scripting for routing and traffic control",
322
+ installable: true,
323
+ category: "infrastructure"
324
+ },
325
+ {
326
+ name: "certbot",
327
+ label: "Certbot",
328
+ description: "Let's Encrypt certificate provisioning",
329
+ installable: true,
330
+ category: "infrastructure"
331
+ },
332
+ {
333
+ name: "rsync",
334
+ label: "rsync",
335
+ description: "Fast directory sync for remote local-build transfers",
336
+ installable: true,
337
+ category: "infrastructure"
338
+ }
339
+ ];
340
+ var SYSTEM_COMPONENTS_BY_NAME = new Map(
341
+ SYSTEM_COMPONENTS.map((component) => [component.name, component])
342
+ );
343
+ function getSystemComponentDefinition(name) {
344
+ return SYSTEM_COMPONENTS_BY_NAME.get(name) ?? {
345
+ name,
346
+ label: name,
347
+ description: `${name} component`,
348
+ installable: false,
349
+ category: "infrastructure"
350
+ };
351
+ }
352
+
353
+ // ../../packages/adapters/src/system/checks.ts
354
+ async function tryExec2(executor, command) {
355
+ const startedAt = Date.now();
356
+ systemDebug("checks", `exec:start ${command}`);
357
+ try {
358
+ const result = await executor.exec(command, { timeout: 1e4 });
359
+ systemDebug(
360
+ "checks",
361
+ `exec:ok ${command} (${formatDuration(startedAt)})`
362
+ );
363
+ return result;
364
+ } catch (err) {
365
+ if (isRemoteConnectionError(err)) {
366
+ systemDebug(
367
+ "checks",
368
+ `exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
369
+ );
370
+ throw err;
371
+ }
372
+ const msg = safeErrorMessage(err);
373
+ systemDebug(
374
+ "checks",
375
+ `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
376
+ );
377
+ return null;
378
+ }
379
+ }
380
+ function healthy(name, version, running) {
381
+ const component = getSystemComponentDefinition(name);
382
+ return {
383
+ name,
384
+ label: component.label,
385
+ description: component.description,
386
+ installable: component.installable,
387
+ installed: true,
388
+ version,
389
+ running,
390
+ healthy: running !== void 0 ? running : true,
391
+ message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
392
+ };
393
+ }
394
+ function unhealthy(name, message, opts) {
395
+ const component = getSystemComponentDefinition(name);
396
+ return {
397
+ name,
398
+ label: component.label,
399
+ description: component.description,
400
+ installable: component.installable,
401
+ installed: !!opts?.version,
402
+ version: opts?.version,
403
+ running: opts?.running,
404
+ healthy: false,
405
+ message
406
+ };
407
+ }
408
+ async function checkDocker(executor) {
409
+ const startedAt = Date.now();
410
+ const recipe = systemCatalog.checks.docker;
411
+ const version = await tryExec2(executor, recipe.versionCommand);
412
+ if (!version) {
413
+ systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
414
+ return unhealthy("docker", recipe.missingMessage);
415
+ }
416
+ const parsed = recipe.parseVersion(version);
417
+ const info = await tryExec2(executor, recipe.daemonCommand);
418
+ if (!info) {
419
+ systemDebug("checks", `docker:not-running (${formatDuration(startedAt)})`);
420
+ return unhealthy("docker", recipe.notRunningMessage, {
421
+ version: parsed,
422
+ running: false
423
+ });
424
+ }
425
+ systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
426
+ return healthy("docker", parsed, true);
427
+ }
428
+ async function checkGit(executor) {
429
+ const startedAt = Date.now();
430
+ const recipe = systemCatalog.checks.git;
431
+ const version = await tryExec2(executor, recipe.versionCommand);
432
+ if (!version) {
433
+ systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
434
+ return unhealthy("git", recipe.missingMessage);
435
+ }
436
+ const parsed = recipe.parseVersion(version);
437
+ systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
438
+ return healthy("git", parsed);
439
+ }
440
+ async function checkRsync(executor) {
441
+ const startedAt = Date.now();
442
+ const recipe = systemCatalog.checks.rsync;
443
+ const version = await tryExec2(executor, recipe.versionCommand);
444
+ if (!version) {
445
+ systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
446
+ return unhealthy("rsync", recipe.missingMessage);
447
+ }
448
+ const parsed = recipe.parseVersion(version);
449
+ systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
450
+ return healthy("rsync", parsed);
451
+ }
452
+ async function checkOpenResty(executor) {
453
+ const startedAt = Date.now();
454
+ const recipe = systemCatalog.checks.openresty;
455
+ const version = await tryExec2(executor, recipe.versionCommand);
456
+ if (!version) {
457
+ systemDebug("checks", `openresty:missing (${formatDuration(startedAt)})`);
458
+ return unhealthy("openresty", recipe.missingMessage);
459
+ }
460
+ const parsed = recipe.parseVersion(version);
461
+ const runningChecks = await Promise.all(
462
+ recipe.runningCommands.map((command) => tryExec2(executor, command))
463
+ );
464
+ const running = runningChecks.some(Boolean);
465
+ if (!running) {
466
+ systemDebug("checks", `openresty:not-running (${formatDuration(startedAt)})`);
467
+ return unhealthy("openresty", recipe.notRunningMessage, {
468
+ version: parsed,
469
+ running: false
470
+ });
471
+ }
472
+ const hasLua = await tryExec2(
473
+ executor,
474
+ `test -f ${OPENRESTY_LUA_DIR}/site_logger.lua && test -f ${OPENRESTY_LUA_DIR}/pipe_stream.lua && echo ok`
475
+ );
476
+ if (!hasLua) {
477
+ systemDebug("checks", `openresty:missing-lua (${formatDuration(startedAt)})`);
478
+ return unhealthy(
479
+ "openresty",
480
+ "OpenResty is running but analytics scripts are not deployed - reinstall to fix",
481
+ { version: parsed, running: true }
482
+ );
483
+ }
484
+ systemDebug("checks", `openresty:healthy (${formatDuration(startedAt)})`);
485
+ return healthy("openresty", parsed, true);
486
+ }
487
+ async function checkCertbot(executor) {
488
+ const startedAt = Date.now();
489
+ const recipe = systemCatalog.checks.certbot;
490
+ const version = await tryExec2(executor, recipe.versionCommand);
491
+ if (!version) {
492
+ systemDebug("checks", `certbot:missing (${formatDuration(startedAt)})`);
493
+ return unhealthy("certbot", recipe.missingMessage);
494
+ }
495
+ const parsed = recipe.parseVersion(version);
496
+ systemDebug("checks", `certbot:healthy (${formatDuration(startedAt)})`);
497
+ return healthy("certbot", parsed);
498
+ }
499
+ var COMPONENT_CHECKS = {
500
+ docker: checkDocker,
501
+ openresty: checkOpenResty,
502
+ certbot: checkCertbot,
503
+ git: checkGit,
504
+ rsync: checkRsync
505
+ };
506
+ async function mapWithConcurrency(items, limit, fn) {
507
+ const results = new Array(items.length);
508
+ let cursor = 0;
509
+ const worker = async () => {
510
+ while (cursor < items.length) {
511
+ const index = cursor++;
512
+ results[index] = await fn(items[index]);
513
+ }
514
+ };
515
+ await Promise.all(
516
+ Array.from({ length: Math.min(limit, items.length) }, worker)
517
+ );
518
+ return results;
519
+ }
520
+ var CHECK_CONCURRENCY = 4;
521
+ async function checkAll(executor) {
522
+ const startedAt = Date.now();
523
+ const entries = SYSTEM_COMPONENTS.map((component) => [component.name, COMPONENT_CHECKS[component.name]]).filter((entry) => Boolean(entry[1]));
524
+ systemDebug(
525
+ "checks",
526
+ `checkAll:start [${entries.map(([name]) => name).join(", ")}]`
527
+ );
528
+ const results = await mapWithConcurrency(
529
+ entries,
530
+ CHECK_CONCURRENCY,
531
+ ([, fn]) => fn(executor)
532
+ );
533
+ await enrichAvailable(executor, results);
534
+ systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
535
+ return results;
536
+ }
537
+ async function enrichAvailable(executor, results) {
538
+ try {
539
+ const profile = await resolveEnvironment(executor);
540
+ await enrichAvailableVersions(executor, profile, results);
541
+ } catch {
542
+ }
543
+ }
544
+ async function checkComponents(executor, names) {
545
+ const startedAt = Date.now();
546
+ const fns = names.map((name) => COMPONENT_CHECKS[name]).filter((fn) => Boolean(fn));
547
+ systemDebug("checks", `checkComponents:start [${names.join(", ")}]`);
548
+ const results = await mapWithConcurrency(
549
+ fns,
550
+ CHECK_CONCURRENCY,
551
+ (fn) => fn(executor)
552
+ );
553
+ await enrichAvailable(executor, results);
554
+ systemDebug(
555
+ "checks",
556
+ `checkComponents:done [${names.join(", ")}] (${formatDuration(startedAt)})`
557
+ );
558
+ return results;
559
+ }
560
+
561
+ // ../../packages/adapters/src/system/elevated-executor.ts
562
+ var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
563
+ function elevateCommand(command) {
564
+ return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
565
+ }
566
+ function dirOf(path) {
567
+ const idx = path.lastIndexOf("/");
568
+ if (idx < 0) return ".";
569
+ return idx === 0 ? "/" : path.slice(0, idx);
570
+ }
571
+ function elevatedExecutor(inner) {
572
+ const writeFileElevated = async (path, content) => {
573
+ const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
574
+ await inner.writeFile(tmp, content);
575
+ await inner.exec(
576
+ elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
577
+ );
578
+ };
579
+ const overrides = {
580
+ exec: (command, opts) => inner.exec(elevateCommand(command), opts),
581
+ streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
582
+ writeFile: writeFileElevated,
583
+ mkdir: async (path) => {
584
+ await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
585
+ },
586
+ rm: async (path) => {
587
+ await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
588
+ }
589
+ };
590
+ return new Proxy(inner, {
591
+ get(target, prop) {
592
+ if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
593
+ return overrides[prop];
594
+ }
595
+ const value = Reflect.get(target, prop, target);
596
+ return typeof value === "function" ? value.bind(target) : value;
597
+ }
598
+ });
599
+ }
600
+
601
+ // ../../packages/adapters/src/system/proxy/detect.ts
602
+ var EDGE_PORTS = [80, 443];
603
+ var EdgeConflictError = class extends AppError {
604
+ constructor(status) {
605
+ super(
606
+ `Ports ${status.occupants.map((o) => o.port).join("/") || "80/443"} are in use by another service (${status.classification}). Accept a migrate or takeover to continue.`,
607
+ 409,
608
+ "EDGE_CONFLICT"
609
+ );
610
+ this.status = status;
611
+ this.name = "EdgeConflictError";
612
+ }
613
+ };
614
+ var EdgeMigrateRequested = class extends Error {
615
+ constructor(status, sites, warnings = []) {
616
+ super("Edge migration requested by user");
617
+ this.status = status;
618
+ this.sites = sites;
619
+ this.warnings = warnings;
620
+ this.name = "EdgeMigrateRequested";
621
+ }
622
+ };
623
+ async function tryExec3(executor, command) {
624
+ try {
625
+ return await executor.exec(command);
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+ function classifyProxy(text) {
631
+ if (!text) return void 0;
632
+ const t = text.toLowerCase();
633
+ if (t.includes("openresty")) return "openresty";
634
+ if (/(^|[\s/:])nginx/.test(t)) return "nginx";
635
+ if (/(^|[\s/:])caddy/.test(t)) return "caddy";
636
+ if (/(apache2|httpd)/.test(t)) return "apache";
637
+ if (/(^|[\s/:])traefik/.test(t)) return "traefik";
638
+ if (/(^|[\s/:])haproxy/.test(t)) return "haproxy";
639
+ return void 0;
640
+ }
641
+ function sq2(value) {
642
+ return `'${value.replace(/'/g, "'\\''")}'`;
643
+ }
644
+ function isOurEdgeContainer(name, image) {
645
+ return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
646
+ }
647
+ async function ourEdgeContainerRunning(executor) {
648
+ const byName = await tryExec3(
649
+ executor,
650
+ `docker ps --filter name=openship-edge --format '{{.Names}}' 2>/dev/null | head -1`
651
+ );
652
+ if (byName && byName.trim()) return true;
653
+ const all = await tryExec3(executor, `docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null`);
654
+ if (!all) return false;
655
+ return all.split("\n").map((line) => line.trim()).filter(Boolean).some((line) => {
656
+ const [name, image] = line.split(" ");
657
+ return isOurEdgeContainer(name, image);
658
+ });
659
+ }
660
+ async function ourLuaOnHost(executor) {
661
+ const lua = await tryExec3(
662
+ executor,
663
+ `test -f ${OPENRESTY_LUA_DIR}/site_logger.lua && echo ok`
664
+ );
665
+ return Boolean(lua && lua.includes("ok"));
666
+ }
667
+ async function detectDockerOnPort(executor, port) {
668
+ const out = await tryExec3(
669
+ executor,
670
+ `docker ps --filter publish=${port} --format '{{.Names}} {{.Image}}' 2>/dev/null | head -1`
671
+ );
672
+ const line = out?.trim();
673
+ if (!line) return null;
674
+ const [name, image] = line.split(" ");
675
+ if (!name) return null;
676
+ return { name, image: image ?? "" };
677
+ }
678
+ async function listenerIsOurOpenResty(executor, listener) {
679
+ if (!listener) return false;
680
+ if (/openresty/i.test(`${listener.rawCommand ?? ""} ${listener.command ?? ""}`)) return true;
681
+ if (listener.pid) {
682
+ const exe = await tryExec3(executor, `readlink -f /proc/${listener.pid}/exe 2>/dev/null || true`);
683
+ if (exe && /openresty/i.test(exe)) return true;
684
+ }
685
+ return false;
686
+ }
687
+ var WORKER_RE = /\b(?:nginx|openresty)\s*:\s*worker process/i;
688
+ var MASTER_RE = /\b(?:nginx|openresty)\s*:\s*master process/i;
689
+ async function resolveProxyMaster(executor, listener) {
690
+ if (!listener?.pid) return listener;
691
+ if (!WORKER_RE.test(`${listener.rawCommand ?? ""} ${listener.command ?? ""}`)) return listener;
692
+ const ppidRaw = await tryExec3(
693
+ executor,
694
+ `awk '/^PPid:/{print $2}' /proc/${listener.pid}/status 2>/dev/null || true`
695
+ );
696
+ const ppid = Number.parseInt((ppidRaw ?? "").trim(), 10);
697
+ if (!Number.isInteger(ppid) || ppid <= 1) return listener;
698
+ const master = await describeProcess(executor, ppid);
699
+ if (!master || !MASTER_RE.test(`${master.rawCommand ?? ""} ${master.command ?? ""}`)) {
700
+ return listener;
701
+ }
702
+ return master;
703
+ }
704
+ async function probeEdgePort(executor, port, ours) {
705
+ const listener = await resolveProxyMaster(executor, await probeListeningPort(executor, port));
706
+ const docker = await detectDockerOnPort(executor, port);
707
+ if (!listener && !docker) return null;
708
+ const proxy = classifyProxy(
709
+ [
710
+ docker?.image,
711
+ docker?.name,
712
+ listener?.rawCommand,
713
+ listener?.command,
714
+ listener?.systemdUnit
715
+ ].filter(Boolean).join(" ")
716
+ );
717
+ const foreignDockerProxy = Boolean(docker) && !isOurEdgeContainer(docker?.name, docker?.image);
718
+ const managedByOpenship = isOurEdgeContainer(docker?.name, docker?.image) || ours.containerRunning && !foreignDockerProxy || !docker && ours.luaOnHost && await listenerIsOurOpenResty(executor, listener);
719
+ return {
720
+ port,
721
+ pid: listener?.pid ?? void 0,
722
+ command: docker ? `docker container ${docker.name} (${docker.image})` : listener?.command,
723
+ rawCommand: listener?.rawCommand,
724
+ systemdUnit: listener?.systemdUnit,
725
+ systemdDescription: listener?.systemdDescription,
726
+ isDocker: Boolean(docker),
727
+ containerName: docker?.name,
728
+ proxy,
729
+ managedByOpenship
730
+ };
731
+ }
732
+ async function probeEdge(executor) {
733
+ const containerRunning = await ourEdgeContainerRunning(executor);
734
+ const luaOnHost = await ourLuaOnHost(executor);
735
+ const all = [];
736
+ for (const port of EDGE_PORTS) {
737
+ const occ = await probeEdgePort(executor, port, { containerRunning, luaOnHost });
738
+ if (occ) all.push(occ);
739
+ }
740
+ const foreign = all.filter((o) => !o.managedByOpenship);
741
+ let classification;
742
+ if (all.length === 0) classification = "free";
743
+ else if (foreign.length === 0) classification = "ours";
744
+ else if (foreign.every((o) => o.proxy && o.proxy !== "openresty")) classification = "known";
745
+ else classification = "unknown";
746
+ return {
747
+ classification,
748
+ occupants: foreign,
749
+ canProceedClean: classification === "free" || classification === "ours"
750
+ };
751
+ }
752
+ function stopTargetsForStatus(status) {
753
+ const out = [];
754
+ const seen = /* @__PURE__ */ new Set();
755
+ for (const o of status.occupants) {
756
+ const identity = o.systemdUnit ?? o.containerName ?? (o.pid ? `pid:${o.pid}` : `port:${o.port}`);
757
+ if (seen.has(identity)) continue;
758
+ seen.add(identity);
759
+ out.push({
760
+ port: o.port,
761
+ unit: o.systemdUnit,
762
+ pid: o.pid,
763
+ container: o.containerName,
764
+ // Prefer the proxy kind over the raw cmdline — `nginx (PID 123)` reads
765
+ // better in "Stopping …" than `nginx: master process /usr/sbin/nginx …`.
766
+ label: o.proxy && o.pid ? `${o.proxy} (PID ${o.pid})` : o.command
767
+ });
768
+ }
769
+ return out;
770
+ }
771
+ async function freeEdgeTargets(executor, targets, onLog) {
772
+ for (const t of targets) {
773
+ const where = t.port ? ` (port ${t.port})` : "";
774
+ if (t.container) {
775
+ onLog(`Stopping container ${t.container}${where}...`, "warn");
776
+ await tryExec3(executor, `docker update --restart=no ${sq2(t.container)} 2>/dev/null || true`);
777
+ await tryExec3(executor, `docker stop ${sq2(t.container)} 2>/dev/null || true`);
778
+ } else if (t.unit) {
779
+ onLog(`Stopping & disabling service ${t.unit}${where}...`, "warn");
780
+ await tryExec3(
781
+ executor,
782
+ `systemctl disable --now ${sq2(t.unit)} 2>/dev/null || systemctl stop ${sq2(t.unit)} 2>/dev/null || true; systemctl reset-failed ${sq2(t.unit)} 2>/dev/null || true`
783
+ );
784
+ } else if (t.pid) {
785
+ onLog(`Stopping ${t.label ?? `process ${t.pid}`}${where}...`, "warn");
786
+ await tryExec3(executor, `kill ${t.pid} 2>/dev/null || true`);
787
+ await new Promise((r) => setTimeout(r, 800));
788
+ await tryExec3(executor, `kill -9 ${t.pid} 2>/dev/null || true`);
789
+ }
790
+ }
791
+ await new Promise((r) => setTimeout(r, 1e3));
792
+ }
793
+
794
+ // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
795
+ async function tryExec4(executor, command) {
796
+ try {
797
+ return await executor.exec(command);
798
+ } catch {
799
+ return null;
800
+ }
801
+ }
802
+ function stripComments(text) {
803
+ return text.replace(/#.*$/gm, "");
804
+ }
805
+ function extractBlocks(text, keyword) {
806
+ const blocks = [];
807
+ const re = new RegExp(`(?:^|[\\s;}])${keyword}\\b[^{;]*\\{`, "g");
808
+ let m;
809
+ while ((m = re.exec(text)) !== null) {
810
+ const openIdx = m.index + m[0].length - 1;
811
+ let depth = 1;
812
+ let i = openIdx + 1;
813
+ for (; i < text.length && depth > 0; i++) {
814
+ if (text[i] === "{") depth++;
815
+ else if (text[i] === "}") depth--;
816
+ }
817
+ if (depth === 0) {
818
+ blocks.push(text.slice(openIdx + 1, i - 1));
819
+ re.lastIndex = i;
820
+ } else {
821
+ break;
822
+ }
823
+ }
824
+ return blocks;
825
+ }
826
+ function sq3(value) {
827
+ return `'${value.replace(/'/g, "'\\''")}'`;
828
+ }
829
+
830
+ // ../../packages/adapters/src/system/proxy/import/nginx.ts
831
+ async function loadNginxConfig(executor) {
832
+ const dumped = await tryExec4(executor, "nginx -T 2>/dev/null");
833
+ if (dumped && /server\s*\{/.test(dumped)) return dumped;
834
+ const cat = await tryExec4(
835
+ executor,
836
+ "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
837
+ );
838
+ return cat ?? "";
839
+ }
840
+ function firstDirective(body, name) {
841
+ const m = body.match(new RegExp(`(?:^|[;{\\s])${name}\\s+([^;]+);`));
842
+ return m?.[1]?.trim();
843
+ }
844
+ function parseUpstreams(config) {
845
+ const map = /* @__PURE__ */ new Map();
846
+ const re = /(?:^|[\s;}])upstream\s+(\S+)\s*\{([^}]*)\}/g;
847
+ let m;
848
+ while ((m = re.exec(config)) !== null) {
849
+ const name = m[1];
850
+ const server = m[2].match(/(?:^|[\s;{])server\s+([^;\s]+)/);
851
+ if (name && server?.[1]) map.set(name, server[1].trim());
852
+ }
853
+ return map;
854
+ }
855
+ function resolveProxyTarget(proxyPass, upstreams) {
856
+ const raw = proxyPass.replace(/;$/, "").trim();
857
+ if (raw.includes("$")) return { reason: `proxy_pass "${raw}" uses an nginx variable` };
858
+ if (/\/\/unix:/i.test(raw)) return { reason: `proxy_pass "${raw}" targets a unix socket` };
859
+ const m = raw.match(/^(https?:\/\/)([^/]+)(\/.*)?$/i);
860
+ if (!m) return { reason: `unrecognized proxy_pass "${raw}"` };
861
+ const scheme = m[1];
862
+ const authority = m[2];
863
+ const host = authority.replace(/:\d+$/, "");
864
+ if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
865
+ const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
866
+ if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
867
+ return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
868
+ }
869
+ function extractLocationProxies(serverBody) {
870
+ const out = [];
871
+ const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
872
+ let m;
873
+ while ((m = re.exec(serverBody)) !== null) {
874
+ const path = m[1].trim();
875
+ const openIdx = m.index + m[0].length - 1;
876
+ let depth = 1;
877
+ let i = openIdx + 1;
878
+ for (; i < serverBody.length && depth > 0; i++) {
879
+ if (serverBody[i] === "{") depth++;
880
+ else if (serverBody[i] === "}") depth--;
881
+ }
882
+ if (depth !== 0) break;
883
+ const locBody = serverBody.slice(openIdx + 1, i - 1);
884
+ re.lastIndex = i;
885
+ const pp = firstDirective(locBody, "proxy_pass");
886
+ if (pp) out.push({ path, proxyPass: pp });
887
+ }
888
+ return out;
889
+ }
890
+ function isHttpsUpgradeForSelf(body, names) {
891
+ const targets = [
892
+ ...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
893
+ ...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
894
+ ].map((m) => m[1].trim());
895
+ if (targets.length === 0) return false;
896
+ return targets.every((raw) => {
897
+ if (!/^https:\/\//i.test(raw)) return false;
898
+ const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
899
+ const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(/:\d+$/, "");
900
+ if (host === "$host" || host === "$server_name" || host === "$http_host") return true;
901
+ return names.includes(host);
902
+ });
903
+ }
904
+ function parseServer(body, source, upstreams) {
905
+ const warnings = [];
906
+ const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
907
+ const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
908
+ const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
909
+ const root = firstDirective(body, "root");
910
+ const certPath = firstDirective(body, "ssl_certificate");
911
+ const keyPath = firstDirective(body, "ssl_certificate_key");
912
+ if (names.length === 0) return { warnings: [] };
913
+ const rawTargets = extractLocationProxies(body);
914
+ if (rawTargets.length === 0) {
915
+ const serverLevel = firstDirective(body, "proxy_pass");
916
+ if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
917
+ }
918
+ const resolved = [];
919
+ for (const t of rawTargets) {
920
+ const r = resolveProxyTarget(t.proxyPass, upstreams);
921
+ if ("reason" in r) warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
922
+ else resolved.push({ path: t.path, url: r.url });
923
+ }
924
+ let target;
925
+ let routes;
926
+ if (resolved.length > 0) {
927
+ const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
928
+ target = { kind: "proxy", url: primary.url };
929
+ routes = resolved;
930
+ } else if (root) {
931
+ target = { kind: "static", root: root.replace(/;$/, "") };
932
+ } else if (isHttpsUpgradeForSelf(body, names)) {
933
+ return { warnings: [] };
934
+ } else {
935
+ warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
936
+ return { warnings };
937
+ }
938
+ const site = { serverNames: names, ssl, target, source };
939
+ if (routes) site.routes = routes;
940
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
941
+ return { site, warnings };
942
+ }
943
+ function parseNginxConfig(raw) {
944
+ const warnings = [];
945
+ const sites = [];
946
+ if (!raw.trim()) {
947
+ return { proxy: "nginx", sites, warnings: ["nginx: no readable configuration found"] };
948
+ }
949
+ const config = stripComments(raw);
950
+ const upstreams = parseUpstreams(config);
951
+ const blocks = extractBlocks(config, "server");
952
+ if (blocks.length === 0) {
953
+ warnings.push("nginx: no server blocks found");
954
+ }
955
+ for (const body of blocks) {
956
+ const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
957
+ warnings.push(...blockWarnings);
958
+ if (site) sites.push(site);
959
+ }
960
+ return { proxy: "nginx", sites, warnings };
961
+ }
962
+ async function scanNginx(executor) {
963
+ return parseNginxConfig(await loadNginxConfig(executor));
964
+ }
965
+
966
+ // ../../packages/adapters/src/system/proxy/import/caddy.ts
967
+ var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
968
+ async function loadCaddyJson(executor) {
969
+ for (const p of CADDYFILE_PATHS) {
970
+ const out = await tryExec4(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
971
+ if (out && out.trim().startsWith("{")) return out;
972
+ }
973
+ return null;
974
+ }
975
+ function flattenHandlers(handlers, out = []) {
976
+ for (const h of handlers ?? []) {
977
+ if (h.handler === "subroute") {
978
+ for (const r of h.routes ?? []) flattenHandlers(r.handle, out);
979
+ } else {
980
+ out.push(h);
981
+ }
982
+ }
983
+ return out;
984
+ }
985
+ function targetFromRoute(route) {
986
+ const flat = flattenHandlers(route.handle);
987
+ const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
988
+ if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
989
+ const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
990
+ if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
991
+ return null;
992
+ }
993
+ function parseCaddyJson(raw) {
994
+ let cfg;
995
+ try {
996
+ cfg = JSON.parse(raw);
997
+ } catch {
998
+ return null;
999
+ }
1000
+ const servers = cfg.apps?.http?.servers;
1001
+ if (!servers) return null;
1002
+ const sites = [];
1003
+ for (const srv of Object.values(servers)) {
1004
+ const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
1005
+ for (const route of srv.routes ?? []) {
1006
+ const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
1007
+ if (hosts.length === 0) continue;
1008
+ const target = targetFromRoute(route);
1009
+ if (!target) continue;
1010
+ sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
1011
+ }
1012
+ }
1013
+ return { sites, warnings: [] };
1014
+ }
1015
+ async function loadCaddyfile(executor) {
1016
+ for (const p of CADDYFILE_PATHS) {
1017
+ const out = await tryExec4(executor, `cat ${p} 2>/dev/null`);
1018
+ if (out && out.trim()) return out;
1019
+ }
1020
+ return "";
1021
+ }
1022
+ function caddyBlocks(text) {
1023
+ const out = [];
1024
+ let i = 0;
1025
+ let headerStart = 0;
1026
+ while (i < text.length) {
1027
+ if (text[i] === "{") {
1028
+ const header = text.slice(headerStart, i).trim();
1029
+ let depth = 1;
1030
+ let j = i + 1;
1031
+ for (; j < text.length && depth > 0; j++) {
1032
+ if (text[j] === "{") depth++;
1033
+ else if (text[j] === "}") depth--;
1034
+ }
1035
+ if (depth !== 0) break;
1036
+ out.push({ header, body: text.slice(i + 1, j - 1) });
1037
+ i = j;
1038
+ headerStart = j;
1039
+ } else {
1040
+ i++;
1041
+ }
1042
+ }
1043
+ return out;
1044
+ }
1045
+ function parseAddresses(header) {
1046
+ return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
1047
+ const httpScheme = /^http:\/\//i.test(addr);
1048
+ const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
1049
+ const isPort80 = /:80$/.test(addr);
1050
+ return { host, ssl: !httpScheme && !isPort80 };
1051
+ }).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
1052
+ }
1053
+ async function scanCaddy(executor) {
1054
+ const json = await loadCaddyJson(executor);
1055
+ if (json) {
1056
+ const parsed = parseCaddyJson(json);
1057
+ if (parsed && parsed.sites.length > 0) {
1058
+ return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
1059
+ }
1060
+ }
1061
+ return scanCaddyText(executor);
1062
+ }
1063
+ async function scanCaddyText(executor) {
1064
+ const raw = await loadCaddyfile(executor);
1065
+ const warnings = [];
1066
+ const sites = [];
1067
+ if (!raw.trim()) {
1068
+ return { proxy: "caddy", sites, warnings: ["caddy: no readable Caddyfile found"] };
1069
+ }
1070
+ const text = stripComments(raw);
1071
+ let blocks = caddyBlocks(text);
1072
+ if (blocks.length === 0) {
1073
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
1074
+ if (lines.length > 0) {
1075
+ blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
1076
+ } else {
1077
+ warnings.push("caddy: config present but no parseable site blocks");
1078
+ }
1079
+ }
1080
+ for (const { header, body } of blocks) {
1081
+ if (!header) continue;
1082
+ const addrs = parseAddresses(header);
1083
+ if (addrs.length === 0) {
1084
+ warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
1085
+ continue;
1086
+ }
1087
+ const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
1088
+ const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
1089
+ const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
1090
+ let target;
1091
+ if (proxyMatch) {
1092
+ const upstream = proxyMatch[1].trim().split(/\s+/)[0];
1093
+ target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
1094
+ } else if (rootMatch) {
1095
+ target = { kind: "static", root: rootMatch[1] };
1096
+ } else {
1097
+ warnings.push(`caddy: ${addrs[0].host} has no reverse_proxy or root \u2014 skipped`);
1098
+ continue;
1099
+ }
1100
+ const site = {
1101
+ serverNames: addrs.map((a) => a.host),
1102
+ ssl: addrs.some((a) => a.ssl),
1103
+ target,
1104
+ source: "caddy"
1105
+ };
1106
+ if (tlsMatch && tlsMatch[1].includes("/")) {
1107
+ site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
1108
+ }
1109
+ sites.push(site);
1110
+ }
1111
+ return { proxy: "caddy", sites, warnings };
1112
+ }
1113
+
1114
+ // ../../packages/adapters/src/system/proxy/import/apache.ts
1115
+ async function vhostFilesFromDump(executor) {
1116
+ const dump = await tryExec4(
1117
+ executor,
1118
+ "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
1119
+ );
1120
+ if (!dump) return [];
1121
+ const files = /* @__PURE__ */ new Set();
1122
+ for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
1123
+ const f = m[1].trim();
1124
+ if (f.startsWith("/")) files.add(f);
1125
+ }
1126
+ return [...files];
1127
+ }
1128
+ async function loadApacheConfig(executor) {
1129
+ const files = await vhostFilesFromDump(executor);
1130
+ if (files.length > 0) {
1131
+ const cat2 = await tryExec4(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
1132
+ if (cat2 && cat2.trim()) return cat2;
1133
+ }
1134
+ const cat = await tryExec4(
1135
+ executor,
1136
+ "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
1137
+ );
1138
+ return cat ?? "";
1139
+ }
1140
+ function directive(body, name) {
1141
+ const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
1142
+ return m?.[1]?.trim();
1143
+ }
1144
+ function directiveAll(body, name) {
1145
+ return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
1146
+ (m) => m[1].trim()
1147
+ );
1148
+ }
1149
+ function parseVhost(body, portHint) {
1150
+ const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
1151
+ const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
1152
+ const names = [serverName, ...aliases].filter((n) => Boolean(n));
1153
+ if (names.length === 0) {
1154
+ return { warning: "apache: skipped a VirtualHost with no ServerName" };
1155
+ }
1156
+ const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
1157
+ const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
1158
+ const certPath = directive(body, "SSLCertificateFile");
1159
+ const keyPath = directive(body, "SSLCertificateKeyFile");
1160
+ const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
1161
+ let target;
1162
+ if (routes.length > 0) {
1163
+ const primary = routes.find((r) => r.path === "/") ?? routes[0];
1164
+ target = { kind: "proxy", url: primary.url };
1165
+ } else if (docRoot) {
1166
+ target = { kind: "static", root: docRoot };
1167
+ } else {
1168
+ return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
1169
+ }
1170
+ const site = { serverNames: names, ssl, target, source: "apache" };
1171
+ if (routes.length > 0) site.routes = routes;
1172
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
1173
+ return site;
1174
+ }
1175
+ function vhostBlocks(text) {
1176
+ const out = [];
1177
+ const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
1178
+ let m;
1179
+ while ((m = re.exec(text)) !== null) {
1180
+ out.push({ args: m[1].trim(), body: m[2] });
1181
+ }
1182
+ return out;
1183
+ }
1184
+ async function scanApache(executor) {
1185
+ const raw = await loadApacheConfig(executor);
1186
+ const warnings = [];
1187
+ const sites = [];
1188
+ if (!raw.trim()) {
1189
+ return { proxy: "apache", sites, warnings: ["apache: no readable configuration found"] };
1190
+ }
1191
+ const text = stripComments(raw);
1192
+ const blocks = vhostBlocks(text);
1193
+ if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
1194
+ for (const { args, body } of blocks) {
1195
+ const parsed = parseVhost(body, args);
1196
+ if ("warning" in parsed) warnings.push(parsed.warning);
1197
+ else sites.push(parsed);
1198
+ }
1199
+ return { proxy: "apache", sites, warnings };
1200
+ }
1201
+
1202
+ // ../../packages/adapters/src/system/proxy/import/traefik.ts
1203
+ function extractHosts(rule) {
1204
+ const hosts = [];
1205
+ for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
1206
+ for (const m of call[1].matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
1207
+ const h = m[1].trim();
1208
+ if (h) hosts.push(h);
1209
+ }
1210
+ }
1211
+ return hosts;
1212
+ }
1213
+ function extraMatchers(rule) {
1214
+ const found = /* @__PURE__ */ new Set();
1215
+ for (const m of rule.matchAll(
1216
+ /\b(PathPrefix|PathRegexp|Path|HeadersRegexp|Headers|Method|Query|ClientIP|HostRegexp)\b/gi
1217
+ )) {
1218
+ found.add(m[1]);
1219
+ }
1220
+ return [...found];
1221
+ }
1222
+ function parseTraefikLabels(containers) {
1223
+ const sites = [];
1224
+ const warnings = [];
1225
+ for (const c of containers) {
1226
+ if (c.labels["traefik.enable"] === "false") continue;
1227
+ const routers = /* @__PURE__ */ new Map();
1228
+ const servicePorts = /* @__PURE__ */ new Map();
1229
+ for (const [key, val] of Object.entries(c.labels)) {
1230
+ const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
1231
+ if (r) {
1232
+ const bag = routers.get(r[1]) ?? {};
1233
+ bag[r[2]] = val;
1234
+ routers.set(r[1], bag);
1235
+ continue;
1236
+ }
1237
+ const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
1238
+ if (s) servicePorts.set(s[1], val);
1239
+ }
1240
+ for (const [rname, props] of routers) {
1241
+ const rule = props.rule;
1242
+ if (!rule) continue;
1243
+ const hosts = extractHosts(rule);
1244
+ if (hosts.length === 0) {
1245
+ warnings.push(
1246
+ /HostRegexp/i.test(rule) ? `traefik: router "${rname}" uses HostRegexp (${rule}) \u2014 regex hosts aren't migratable` : `traefik: router "${rname}" has no Host() rule (${rule}) \u2014 skipped`
1247
+ );
1248
+ continue;
1249
+ }
1250
+ if (!c.ip) {
1251
+ warnings.push(
1252
+ `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
1253
+ );
1254
+ continue;
1255
+ }
1256
+ const svc = props.service;
1257
+ const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
1258
+ const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
1259
+ const url = `http://${c.ip}:${port}`;
1260
+ const extras = extraMatchers(rule);
1261
+ if (extras.length > 0) {
1262
+ warnings.push(
1263
+ `traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
1264
+ );
1265
+ }
1266
+ if (props.middlewares) {
1267
+ warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
1268
+ }
1269
+ const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
1270
+ sites.push({
1271
+ serverNames: hosts,
1272
+ ssl,
1273
+ target: { kind: "proxy", url },
1274
+ source: `traefik container ${c.name}`
1275
+ });
1276
+ }
1277
+ }
1278
+ return { proxy: "traefik", sites, warnings };
1279
+ }
1280
+ async function scanTraefik(executor) {
1281
+ const out = await tryExec4(
1282
+ executor,
1283
+ "docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null"
1284
+ );
1285
+ if (!out) {
1286
+ return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
1287
+ }
1288
+ const containers = [];
1289
+ for (const line of out.split("\n")) {
1290
+ const [rawName, rawLabels, rawIps] = line.split(" ");
1291
+ if (!rawName || !rawLabels) continue;
1292
+ let labels;
1293
+ try {
1294
+ labels = JSON.parse(rawLabels) ?? {};
1295
+ } catch {
1296
+ continue;
1297
+ }
1298
+ if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
1299
+ const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
1300
+ containers.push({ name: rawName.replace(/^\//, ""), labels, ip });
1301
+ }
1302
+ return parseTraefikLabels(containers);
1303
+ }
1304
+
1305
+ // ../../packages/adapters/src/system/proxy/import/index.ts
1306
+ var INSTALLED_MARKERS = [
1307
+ { proxy: "nginx", paths: ["/etc/nginx/nginx.conf"] },
1308
+ { proxy: "caddy", paths: ["/etc/caddy/Caddyfile"] },
1309
+ { proxy: "apache", paths: ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf"] }
1310
+ ];
1311
+ async function detectInstalledProxy(executor) {
1312
+ const probe = INSTALLED_MARKERS.map(({ proxy, paths }) => {
1313
+ const test = paths.map((p) => `[ -f '${p}' ]`).join(" || ");
1314
+ return `{ ${test}; } && echo ${proxy}`;
1315
+ }).join("; ");
1316
+ let out = "";
1317
+ try {
1318
+ out = await executor.exec(`{ ${probe}; } 2>/dev/null; true`, { timeout: 1e4 });
1319
+ } catch {
1320
+ return null;
1321
+ }
1322
+ const found = new Set(
1323
+ out.split("\n").map((l) => l.trim()).filter(Boolean)
1324
+ );
1325
+ return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
1326
+ }
1327
+ async function scanImportableSites(executor, proxy) {
1328
+ switch (proxy) {
1329
+ case "nginx":
1330
+ return scanNginx(executor);
1331
+ case "caddy":
1332
+ return scanCaddy(executor);
1333
+ case "apache":
1334
+ return scanApache(executor);
1335
+ case "traefik":
1336
+ return scanTraefik(executor);
1337
+ default:
1338
+ return {
1339
+ proxy,
1340
+ sites: [],
1341
+ warnings: [`${proxy}: config import not supported \u2014 takeover only`]
1342
+ };
1343
+ }
1344
+ }
1345
+ function canImportProxy(proxy) {
1346
+ return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik";
1347
+ }
1348
+
1349
+ // ../../packages/adapters/src/system/proxy/takeover-journal.ts
1350
+ var JOURNAL_DIR = "/var/lib/openship";
1351
+ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
1352
+ function log(message, level = "info") {
1353
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1354
+ }
1355
+ async function tryExec5(executor, cmd) {
1356
+ try {
1357
+ return await executor.exec(cmd);
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
1362
+ async function buildJournal(executor, status) {
1363
+ const units = /* @__PURE__ */ new Map();
1364
+ const containers = /* @__PURE__ */ new Map();
1365
+ const processes = /* @__PURE__ */ new Map();
1366
+ for (const o of status.occupants) {
1367
+ if (o.containerName && !containers.has(o.containerName)) {
1368
+ const r = await tryExec5(
1369
+ executor,
1370
+ `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
1371
+ );
1372
+ containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
1373
+ } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
1374
+ const en = await tryExec5(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
1375
+ units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
1376
+ } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
1377
+ processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
1378
+ }
1379
+ }
1380
+ return {
1381
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1382
+ units: [...units.values()],
1383
+ containers: [...containers.values()],
1384
+ processes: [...processes.values()]
1385
+ };
1386
+ }
1387
+ async function writeJournal(executor, journal) {
1388
+ try {
1389
+ await executor.mkdir(JOURNAL_DIR);
1390
+ await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ async function clearJournal(executor) {
1395
+ await tryExec5(executor, `rm -f ${JOURNAL_PATH}`);
1396
+ }
1397
+ async function readJournal(executor) {
1398
+ const raw = await tryExec5(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
1399
+ if (!raw?.trim()) return null;
1400
+ try {
1401
+ return JSON.parse(raw);
1402
+ } catch {
1403
+ await clearJournal(executor);
1404
+ return null;
1405
+ }
1406
+ }
1407
+ async function rollback(executor, journal, onLog) {
1408
+ onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
1409
+ await tryExec5(
1410
+ executor,
1411
+ "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
1412
+ );
1413
+ for (const u of journal.units) {
1414
+ await tryExec5(
1415
+ executor,
1416
+ u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
1417
+ );
1418
+ }
1419
+ for (const c of journal.containers) {
1420
+ await tryExec5(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
1421
+ await tryExec5(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
1422
+ }
1423
+ for (const p of journal.processes ?? []) {
1424
+ if (p.command) {
1425
+ await tryExec5(
1426
+ executor,
1427
+ `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
1428
+ );
1429
+ } else {
1430
+ onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1431
+ }
1432
+ }
1433
+ }
1434
+ async function beginEdgeTakeover(executor, status, onLog) {
1435
+ await writeJournal(executor, await buildJournal(executor, status));
1436
+ await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
1437
+ }
1438
+ async function rollbackEdgeTakeover(executor, onLog) {
1439
+ const journal = await readJournal(executor);
1440
+ if (!journal || journal.completed) return false;
1441
+ await rollback(executor, journal, onLog);
1442
+ await clearJournal(executor);
1443
+ return true;
1444
+ }
1445
+ async function completeEdgeTakeover(executor) {
1446
+ const journal = await readJournal(executor);
1447
+ if (journal) {
1448
+ journal.completed = true;
1449
+ await writeJournal(executor, journal);
1450
+ }
1451
+ await clearJournal(executor);
1452
+ }
1453
+ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
1454
+ const journal = await readJournal(executor);
1455
+ if (!journal) return;
1456
+ if (journal.completed) {
1457
+ await clearJournal(executor);
1458
+ return;
1459
+ }
1460
+ if (isEdgeHealthy && await isEdgeHealthy()) {
1461
+ onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
1462
+ await clearJournal(executor);
1463
+ return;
1464
+ }
1465
+ onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
1466
+ await rollback(executor, journal, onLog);
1467
+ await clearJournal(executor);
1468
+ }
1469
+
1470
+ // ../../packages/adapters/src/system/proxy/index.ts
1471
+ async function importSites(executor, status) {
1472
+ const proxy = status.occupants.find((o) => o.proxy)?.proxy;
1473
+ if (!proxy) return { proxy: "nginx", sites: [], warnings: [] };
1474
+ if (canImportProxy(proxy)) return scanImportableSites(executor, proxy);
1475
+ return { proxy, sites: [], warnings: [`${proxy}: config import not supported \u2014 takeover only`] };
1476
+ }
1477
+ async function foreignProxyOnEdge(executor) {
1478
+ const status = await probeEdge(executor);
1479
+ const blocked = !status.canProceedClean && status.occupants.length > 0;
1480
+ return { status, blocked, owner: describeEdgeOwner(status.occupants) };
1481
+ }
1482
+ function describeEdgeOwner(occupants) {
1483
+ const labels = [];
1484
+ const seen = /* @__PURE__ */ new Set();
1485
+ for (const o of occupants) {
1486
+ const identity = o.systemdUnit ?? o.containerName ?? (o.pid ? `pid:${o.pid}` : o.command ?? `port:${o.port}`);
1487
+ if (seen.has(identity)) continue;
1488
+ seen.add(identity);
1489
+ labels.push(
1490
+ o.systemdUnit ?? (o.containerName ? `docker container ${o.containerName}` : void 0) ?? (o.proxy && o.pid ? `${o.proxy} (PID ${o.pid})` : void 0) ?? o.proxy ?? o.command ?? `port ${o.port}`
1491
+ );
1492
+ }
1493
+ return labels.join(", ");
1494
+ }
1495
+
1496
+ // ../../packages/adapters/src/system/proxy/consent.ts
1497
+ function log2(message, level = "info") {
1498
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1499
+ }
1500
+ async function ensureEdgeClear(executor, config, onLog) {
1501
+ const status = await probeEdge(executor);
1502
+ if (status.canProceedClean) return { tookOver: false };
1503
+ const takeover = async () => {
1504
+ onLog(log2(
1505
+ `Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
1506
+ "warn"
1507
+ ));
1508
+ const configured = config?.edgePolicy?.stopTargets ?? [];
1509
+ const targets = configured.length ? configured : stopTargetsForStatus(status);
1510
+ await freeEdgeTargets(executor, targets, (m, l) => onLog(log2(m, l)));
1511
+ };
1512
+ if (config?.edgePolicy?.mode === "takeover") {
1513
+ await takeover();
1514
+ return { tookOver: true };
1515
+ }
1516
+ if (config?.promptUser) {
1517
+ const known = status.classification === "known";
1518
+ const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
1519
+ const scan = await importSites(executor, status);
1520
+ const migratable = scan.sites.length > 0;
1521
+ const message = migratable ? `Openship runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already serving them (${scan.sites.length} site${scan.sites.length === 1 ? "" : "s"}). Migrate those sites into Openship and take over, just stop it and take over, or cancel?` : known ? `Openship runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already serving them. Stop it and take over, or cancel and leave it running?` : `Openship runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already using them and we can't identify it. Stop it and take over, or cancel and leave it running?`;
1522
+ const details = {
1523
+ edge: status,
1524
+ sites: scan.sites,
1525
+ warnings: scan.warnings
1526
+ };
1527
+ const action = await config.promptUser({
1528
+ promptId: "edge_conflict",
1529
+ title: known ? "Existing reverse proxy detected" : "Ports 80/443 are in use",
1530
+ message,
1531
+ actions: [
1532
+ ...migratable ? [{ id: "migrate", label: `Migrate ${scan.sites.length} site(s) & take over`, variant: "primary" }] : [],
1533
+ { id: "override", label: "Stop it & take over", variant: "danger" },
1534
+ { id: "cancel", label: "Cancel", variant: "secondary" }
1535
+ ],
1536
+ details
1537
+ });
1538
+ if (action === "migrate") {
1539
+ throw new EdgeMigrateRequested(status, scan.sites, scan.warnings);
1540
+ }
1541
+ if (action === "override") {
1542
+ await takeover();
1543
+ return { tookOver: true };
1544
+ }
1545
+ throw new EdgeConflictError(status);
1546
+ }
1547
+ throw new EdgeConflictError(status);
1548
+ }
1549
+
1550
+ // ../../packages/adapters/src/system/installer.ts
1551
+ function log3(message, level = "info") {
1552
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1553
+ }
1554
+ function describeEnvironment(profile) {
1555
+ return `Detected environment: os=${profile.os}, arch=${profile.arch}, distro=${profile.distro ?? "n/a"}, packageManager=${profile.packageManager}, serviceManager=${profile.serviceManager}`;
1556
+ }
1557
+ async function execSafe(executor, cmd) {
1558
+ try {
1559
+ await executor.exec(cmd);
1560
+ } catch {
1561
+ }
1562
+ }
1563
+ async function ensureAptReady(executor, onLog) {
1564
+ const broken = await executor.exec("dpkg --audit 2>&1 | head -1").catch(() => "");
1565
+ if (!broken) return;
1566
+ onLog(log3("Fixing interrupted package state..."));
1567
+ await execSafe(executor, "fuser -k /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true");
1568
+ await execSafe(executor, "rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true");
1569
+ await executor.streamExec("dpkg --configure -a 2>&1", onLog);
1570
+ }
1571
+ async function prepareExecutor(executor, component) {
1572
+ const profile = await resolveEnvironment(executor);
1573
+ if (profile.isRoot) return { ok: true, executor, profile };
1574
+ if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
1575
+ return {
1576
+ ok: false,
1577
+ result: {
1578
+ component,
1579
+ success: false,
1580
+ error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
1581
+ }
1582
+ };
1583
+ }
1584
+ async function installDocker(executor, onLog) {
1585
+ const prep = await prepareExecutor(executor, "docker");
1586
+ if (!prep.ok) return prep.result;
1587
+ executor = prep.executor;
1588
+ const profile = prep.profile;
1589
+ const plan = systemCatalog.installs.docker(profile);
1590
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1591
+ return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
1592
+ }
1593
+ onLog(log3("Installing Docker Engine..."));
1594
+ try {
1595
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1596
+ if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
1597
+ if (plan.startCommand) {
1598
+ onLog(log3("Starting Docker service..."));
1599
+ await executor.streamExec(plan.startCommand, onLog);
1600
+ }
1601
+ const version = await executor.exec(plan.verifyCommand);
1602
+ const parsed = systemCatalog.checks.docker.parseVersion(version);
1603
+ onLog(log3(`Docker ${parsed} installed`));
1604
+ return { component: "docker", success: true, version: parsed };
1605
+ } catch (err) {
1606
+ const msg = safeErrorMessage(err);
1607
+ onLog(log3(`Docker installation failed: ${msg}`, "error"));
1608
+ return { component: "docker", success: false, error: msg };
1609
+ }
1610
+ }
1611
+ async function installGit(executor, onLog, opts) {
1612
+ const prep = await prepareExecutor(executor, "git");
1613
+ if (!prep.ok) return prep.result;
1614
+ executor = prep.executor;
1615
+ const profile = prep.profile;
1616
+ const plan = systemCatalog.installs.git(profile);
1617
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1618
+ return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
1619
+ }
1620
+ onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1621
+ try {
1622
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1623
+ if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
1624
+ const version = await executor.exec(plan.verifyCommand);
1625
+ const parsed = systemCatalog.checks.git.parseVersion(version);
1626
+ onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1627
+ return { component: "git", success: true, version: parsed };
1628
+ } catch (err) {
1629
+ const msg = safeErrorMessage(err);
1630
+ onLog(log3(`Git installation failed: ${msg}`, "error"));
1631
+ return { component: "git", success: false, error: msg };
1632
+ }
1633
+ }
1634
+ async function installRsync(executor, onLog) {
1635
+ const prep = await prepareExecutor(executor, "rsync");
1636
+ if (!prep.ok) return prep.result;
1637
+ executor = prep.executor;
1638
+ const profile = prep.profile;
1639
+ const plan = systemCatalog.installs.rsync(profile);
1640
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1641
+ return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
1642
+ }
1643
+ onLog(log3("Installing rsync..."));
1644
+ try {
1645
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1646
+ if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
1647
+ const version = await executor.exec(plan.verifyCommand);
1648
+ const parsed = systemCatalog.checks.rsync.parseVersion(version);
1649
+ onLog(log3(`rsync ${parsed} installed`));
1650
+ return { component: "rsync", success: true, version: parsed };
1651
+ } catch (err) {
1652
+ const msg = safeErrorMessage(err);
1653
+ onLog(log3(`rsync installation failed: ${msg}`, "error"));
1654
+ return { component: "rsync", success: false, error: msg };
1655
+ }
1656
+ }
1657
+ async function installCertbot(executor, onLog) {
1658
+ const prep = await prepareExecutor(executor, "certbot");
1659
+ if (!prep.ok) return prep.result;
1660
+ executor = prep.executor;
1661
+ const profile = prep.profile;
1662
+ const plan = systemCatalog.installs.certbot(profile);
1663
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1664
+ return { component: "certbot", success: false, error: plan.unsupportedReason ?? "Certbot install not supported" };
1665
+ }
1666
+ onLog(log3("Installing certbot..."));
1667
+ try {
1668
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1669
+ if (code !== 0) return { component: "certbot", success: false, error: "Certbot installation failed" };
1670
+ const version = await executor.exec(plan.verifyCommand);
1671
+ const parsed = systemCatalog.checks.certbot.parseVersion(version);
1672
+ onLog(log3(`Certbot ${parsed} installed`));
1673
+ return { component: "certbot", success: true, version: parsed };
1674
+ } catch (err) {
1675
+ const msg = safeErrorMessage(err);
1676
+ onLog(log3(`Certbot installation failed: ${msg}`, "error"));
1677
+ return { component: "certbot", success: false, error: msg };
1678
+ }
1679
+ }
1680
+ async function installOpenResty(executor, onLog, config) {
1681
+ const prep = await prepareExecutor(executor, "openresty");
1682
+ if (!prep.ok) return prep.result;
1683
+ executor = prep.executor;
1684
+ const profile = prep.profile;
1685
+ onLog(log3(describeEnvironment(profile)));
1686
+ const plan = systemCatalog.installs.openresty(profile);
1687
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1688
+ return { component: "openresty", success: false, error: plan.unsupportedReason ?? "OpenResty install not supported" };
1689
+ }
1690
+ onLog(log3("Installing OpenResty..."));
1691
+ try {
1692
+ const { tookOver } = await ensureEdgeClear(executor, config, onLog);
1693
+ const hasIt = await executor.exec("command -v openresty >/dev/null 2>&1 && echo y || echo n").then((r) => r.trim() === "y");
1694
+ if (hasIt && await ourLuaOnHost(executor)) {
1695
+ onLog(log3("Stopping existing OpenResty..."));
1696
+ await execSafe(executor, "systemctl stop openresty 2>/dev/null || true");
1697
+ await execSafe(executor, "pkill -f '[o]penresty' 2>/dev/null || true");
1698
+ }
1699
+ if (profile.packageManager === "apt") {
1700
+ await ensureAptReady(executor, onLog);
1701
+ }
1702
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1703
+ if (code !== 0) return { component: "openresty", success: false, error: "OpenResty installation failed" };
1704
+ await execSafe(executor, "systemctl stop openresty 2>/dev/null || true");
1705
+ await execSafe(executor, "pkill -f '[o]penresty' 2>/dev/null || true");
1706
+ await execSafe(executor, "systemctl reset-failed openresty 2>/dev/null || true");
1707
+ const paths = await detectOpenRestyPaths(executor);
1708
+ await ensureOpenRestyConfig(executor, paths);
1709
+ onLog(log3("Validating config..."));
1710
+ const { code: testCode } = await executor.streamExec(`${paths.bin} -t 2>&1`, onLog);
1711
+ if (testCode !== 0) {
1712
+ return { component: "openresty", success: false, error: "OpenResty config invalid - see logs above" };
1713
+ }
1714
+ if (plan.startCommand) {
1715
+ onLog(log3("Starting OpenResty..."));
1716
+ let start = await executor.streamExec(plan.startCommand, onLog);
1717
+ if (start.code !== 0 && tookOver) {
1718
+ onLog(log3("Start failed - reclaiming authorized ports...", "warn"));
1719
+ const targets = config?.edgePolicy?.stopTargets?.length ? config.edgePolicy.stopTargets : stopTargetsForStatus(await probeEdge(executor));
1720
+ await freeEdgeTargets(executor, targets, (m, l) => onLog(log3(m, l)));
1721
+ await execSafe(executor, "systemctl reset-failed openresty 2>/dev/null || true");
1722
+ start = await executor.streamExec(plan.startCommand, onLog);
1723
+ }
1724
+ if (start.code !== 0) {
1725
+ const journal = await executor.exec(
1726
+ "journalctl -xeu openresty.service --no-pager -n 30 2>/dev/null || echo '(unavailable)'"
1727
+ ).catch(() => "(could not read journal)");
1728
+ onLog(log3(`Service journal:
1729
+ ${journal}`, "error"));
1730
+ return { component: "openresty", success: false, error: "OpenResty installed but failed to start" };
1731
+ }
1732
+ }
1733
+ await executor.exec(buildReloadCommand(paths));
1734
+ onLog(log3("Deploying analytics scripts..."));
1735
+ await deployLuaScripts(executor, paths);
1736
+ const version = await executor.exec(plan.verifyCommand);
1737
+ const parsed = systemCatalog.checks.openresty.parseVersion(version);
1738
+ onLog(log3(`OpenResty ${parsed} installed`));
1739
+ return { component: "openresty", success: true, version: parsed };
1740
+ } catch (err) {
1741
+ if (err instanceof EdgeMigrateRequested) throw err;
1742
+ const msg = safeErrorMessage(err);
1743
+ onLog(log3(`OpenResty installation failed: ${msg}`, "error"));
1744
+ return { component: "openresty", success: false, error: msg };
1745
+ }
1746
+ }
1747
+ var COMPONENT_INSTALLERS = {
1748
+ docker: (exec, log4) => installDocker(exec, log4),
1749
+ openresty: (exec, log4, config) => installOpenResty(exec, log4, config),
1750
+ certbot: (exec, log4) => installCertbot(exec, log4),
1751
+ git: (exec, log4) => installGit(exec, log4),
1752
+ rsync: (exec, log4) => installRsync(exec, log4)
1753
+ };
1754
+
1755
+ export {
1756
+ systemCatalog,
1757
+ checkGit,
1758
+ checkAll,
1759
+ checkComponents,
1760
+ ourEdgeContainerRunning,
1761
+ detectInstalledProxy,
1762
+ scanImportableSites,
1763
+ beginEdgeTakeover,
1764
+ rollbackEdgeTakeover,
1765
+ completeEdgeTakeover,
1766
+ recoverInterruptedTakeover,
1767
+ importSites,
1768
+ foreignProxyOnEdge,
1769
+ installGit,
1770
+ COMPONENT_INSTALLERS
1771
+ };