@khorsheed/dsh-ankh-guard 0.1.0 → 0.2.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 (62) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.en.md +75 -29
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +74 -29
  5. package/lib/cli.js +2383 -209
  6. package/lib/client.js +257 -0
  7. package/lib/exit-agent.js +5 -2
  8. package/lib/index.js +722 -38
  9. package/lib/invariant.js +1 -1
  10. package/lib/preflight-runner.js +125 -47
  11. package/lib/processes-BjZgJjQr.js +344 -0
  12. package/lib/restart-context-D6nISh28.js +1245 -0
  13. package/lib/restart-context-DUyExi9O.js +1245 -0
  14. package/lib/{state-Dhx9VG44.js → state-4f7yny39.js} +60 -13
  15. package/lib/state-CZMypGkB.js +323 -0
  16. package/lib/test-seam-DnvLWTeO.js +119 -0
  17. package/lib/test-seam-cli.js +24 -0
  18. package/lib/test-seam-dwvaKjRp.js +459 -0
  19. package/lib/test-seam.js +2 -0
  20. package/lib/types/browser-handoff.d.ts +55 -0
  21. package/lib/types/browser-handoff.js +489 -0
  22. package/lib/types/cli.d.ts +34 -4
  23. package/lib/types/cli.js +1487 -225
  24. package/lib/types/client/index.d.ts +15 -0
  25. package/lib/types/client/index.js +264 -0
  26. package/lib/types/deployment-proof.d.ts +24 -0
  27. package/lib/types/deployment-proof.js +314 -0
  28. package/lib/types/exit-agent.js +2 -0
  29. package/lib/types/git.d.ts +12 -3
  30. package/lib/types/git.js +69 -7
  31. package/lib/types/index.d.ts +66 -3
  32. package/lib/types/index.js +157 -39
  33. package/lib/types/launch-spec.d.ts +263 -0
  34. package/lib/types/launch-spec.js +823 -0
  35. package/lib/types/preflight-runner.d.ts +23 -12
  36. package/lib/types/preflight-runner.js +152 -57
  37. package/lib/types/processes.d.ts +38 -6
  38. package/lib/types/processes.js +236 -10
  39. package/lib/types/restart-context.d.ts +50 -0
  40. package/lib/types/restart-context.js +106 -0
  41. package/lib/types/restart-request.d.ts +32 -0
  42. package/lib/types/restart-request.js +128 -0
  43. package/lib/types/state-files.d.ts +30 -0
  44. package/lib/types/state-files.js +55 -0
  45. package/lib/types/state.d.ts +29 -2
  46. package/lib/types/state.js +52 -7
  47. package/lib/types/temp-artifact.d.ts +15 -0
  48. package/lib/types/temp-artifact.js +17 -0
  49. package/lib/types/test-seam-cli.d.ts +3 -0
  50. package/lib/types/test-seam-cli.js +27 -0
  51. package/lib/types/test-seam.d.ts +55 -0
  52. package/lib/types/test-seam.js +112 -0
  53. package/lib/types/transition.d.ts +118 -0
  54. package/lib/types/transition.js +717 -0
  55. package/package.json +29 -9
  56. package/scripts/dsh-watchdog.sh +1388 -80
  57. package/scripts/install-launchd.sh +43 -5
  58. package/scripts/install-systemd.sh +43 -5
  59. package/scripts/on-install.js +1 -1
  60. package/skills/dsh-self-restart-guard/SKILL.md +38 -12
  61. package/lib/processes-hCAmwma-.js +0 -127
  62. package/lib/restart-context-DmnQXNf-.js +0 -421
package/lib/client.js ADDED
@@ -0,0 +1,257 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@khorsheed/dsh-ankh-guard",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region src/client/index.ts
8
+ const ROUTE = "/_ankh-guard/browser-handoff";
9
+ const CAPABILITY_KEY = "ankh-guard.browser-handoff-capability.v1";
10
+ const PENDING_KEY = "ankh-guard.browser-handoff-pending.v1";
11
+ const BOOT_ID_KEY = "ankh-guard.browser-handoff-boot-id.v1";
12
+ const FALLBACK_HASH_KEY = "ankh-guard-handoff";
13
+ const ACTIVE_RETRY_MS = 250;
14
+ const ERROR_RETRY_MIN_MS = 1e3;
15
+ const ERROR_RETRY_MAX_MS = 3e4;
16
+ /** Sustained-failure threshold for the disconnected overlay; transient blips never show it. */
17
+ const DISCONNECTED_OVERLAY_MS = 5e3;
18
+ /** No Cordis services are needed; this is a same-origin browser lifecycle observer. */
19
+ const inject = [];
20
+ function encodeBase64Url(bytes) {
21
+ let binary = "";
22
+ for (const byte of bytes) binary += String.fromCharCode(byte);
23
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
24
+ }
25
+ function freshCapability() {
26
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
27
+ crypto.getRandomValues(bytes);
28
+ return encodeBase64Url(bytes);
29
+ }
30
+ function capability() {
31
+ const existing = sessionStorage.getItem(CAPABILITY_KEY);
32
+ if (existing !== null && /^[A-Za-z0-9_-]{43}$/.test(existing)) return existing;
33
+ const created = freshCapability();
34
+ sessionStorage.setItem(CAPABILITY_KEY, created);
35
+ return created;
36
+ }
37
+ function readPending() {
38
+ try {
39
+ const value = JSON.parse(sessionStorage.getItem(PENDING_KEY) ?? "null");
40
+ if (value?.version !== 1 || typeof value.cutoverId !== "string" || value.channel !== "original-tab" && value.channel !== "fallback-tab" || value.authentication !== "existing-cookie" && value.authentication !== "launch-url") return null;
41
+ if (value.channel === "original-tab" && typeof value.capability !== "string") return null;
42
+ if (value.returnPath !== void 0 && !isSafeReturnPath(value.returnPath)) return null;
43
+ return value;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** Deliberately discard query and fragment because either may hold credentials. */
49
+ function safeReturnPath(href, origin) {
50
+ try {
51
+ const url = new URL(href);
52
+ if (url.origin !== origin || url.pathname === "" || url.pathname.startsWith("//") || url.pathname.includes("\\")) return void 0;
53
+ return url.pathname;
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ function isSafeReturnPath(value) {
59
+ return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && !value.includes("?") && !value.includes("#");
60
+ }
61
+ function writePending(value) {
62
+ sessionStorage.setItem(PENDING_KEY, JSON.stringify(value));
63
+ }
64
+ function fallbackCutoverId(hash) {
65
+ const id = new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash).get(FALLBACK_HASH_KEY);
66
+ return id !== null && /^[A-Za-z0-9._-]{1,160}$/.test(id) ? id : null;
67
+ }
68
+ function consumeFallbackFragment() {
69
+ const cutoverId = fallbackCutoverId(location.hash);
70
+ if (cutoverId === null) return null;
71
+ const url = new URL(location.href);
72
+ url.hash = "";
73
+ history.replaceState(history.state, "", url.href);
74
+ const pending = {
75
+ version: 1,
76
+ cutoverId,
77
+ channel: "fallback-tab",
78
+ authentication: "launch-url"
79
+ };
80
+ writePending(pending);
81
+ return pending;
82
+ }
83
+ function waitingOverlay(kind = "restarting") {
84
+ const existing = document.getElementById("ankh-guard-browser-handoff");
85
+ if (existing !== null) return existing;
86
+ const zh = navigator.language.toLowerCase().startsWith("zh");
87
+ const element = document.createElement("div");
88
+ element.id = "ankh-guard-browser-handoff";
89
+ element.setAttribute("role", "status");
90
+ element.setAttribute("aria-live", "polite");
91
+ element.textContent = kind === "disconnected" ? zh ? "连接已断开,等待服务恢复。" : "Connection lost; waiting for the service to recover." : zh ? "dsh 正在重启;此标签页会自动恢复。" : "dsh is restarting; this tab will recover automatically.";
92
+ element.style.cssText = [
93
+ "position:fixed",
94
+ "inset:0",
95
+ "z-index:2147483647",
96
+ "display:grid",
97
+ "place-items:center",
98
+ "padding:24px",
99
+ "background:rgba(15,18,24,.92)",
100
+ "color:#fff",
101
+ "font:500 16px/1.5 system-ui,sans-serif",
102
+ "text-align:center"
103
+ ].join(";");
104
+ document.documentElement.append(element);
105
+ return element;
106
+ }
107
+ async function post(body) {
108
+ return fetch(ROUTE, {
109
+ method: "POST",
110
+ credentials: "same-origin",
111
+ cache: "no-store",
112
+ headers: { "content-type": "application/json" },
113
+ body: JSON.stringify(body)
114
+ });
115
+ }
116
+ async function acknowledge(pending) {
117
+ return (await post({
118
+ version: 1,
119
+ operation: "ack",
120
+ cutoverId: pending.cutoverId,
121
+ channel: pending.channel,
122
+ authentication: pending.authentication,
123
+ ...pending.capability !== void 0 ? { capability: pending.capability } : {}
124
+ })).status === 204;
125
+ }
126
+ /** Install the polling/reload loop for this tab's lifetime. */
127
+ function apply(_ctx) {
128
+ let disposed = false;
129
+ let timer;
130
+ let pending = consumeFallbackFragment() ?? readPending();
131
+ let cap;
132
+ let errorRetryMs = ERROR_RETRY_MIN_MS;
133
+ let firstFailureAt;
134
+ const noteDisconnected = () => {
135
+ firstFailureAt ??= Date.now();
136
+ if (Date.now() - firstFailureAt >= DISCONNECTED_OVERLAY_MS) waitingOverlay("disconnected");
137
+ };
138
+ const schedule = (delayMs = 0) => {
139
+ if (!disposed) timer = setTimeout(() => {
140
+ tick();
141
+ }, delayMs);
142
+ };
143
+ const tick = async () => {
144
+ if (disposed) return;
145
+ try {
146
+ if (pending !== null) {
147
+ waitingOverlay();
148
+ if (await acknowledge(pending)) {
149
+ const returnPath = pending.returnPath;
150
+ const originalTab = pending.channel === "original-tab";
151
+ sessionStorage.removeItem(PENDING_KEY);
152
+ sessionStorage.removeItem(CAPABILITY_KEY);
153
+ document.getElementById("ankh-guard-browser-handoff")?.remove();
154
+ pending = null;
155
+ if (originalTab && returnPath !== void 0 && safeReturnPath(location.href, location.origin) !== returnPath) {
156
+ location.replace(returnPath);
157
+ return;
158
+ }
159
+ }
160
+ errorRetryMs = ERROR_RETRY_MIN_MS;
161
+ firstFailureAt = void 0;
162
+ schedule(ACTIVE_RETRY_MS);
163
+ return;
164
+ }
165
+ const tabCapability = cap ??= capability();
166
+ const knownBootId = sessionStorage.getItem(BOOT_ID_KEY);
167
+ const response = await post({
168
+ version: 1,
169
+ operation: "poll",
170
+ capability: tabCapability,
171
+ ...knownBootId === null ? {} : { knownBootId }
172
+ });
173
+ if (!response.ok) {
174
+ noteDisconnected();
175
+ schedule(errorRetryMs);
176
+ errorRetryMs = Math.min(errorRetryMs * 2, ERROR_RETRY_MAX_MS);
177
+ return;
178
+ }
179
+ errorRetryMs = ERROR_RETRY_MIN_MS;
180
+ firstFailureAt = void 0;
181
+ const result = await response.json();
182
+ if (typeof result.bootId === "string") sessionStorage.setItem(BOOT_ID_KEY, result.bootId);
183
+ if (result.state === "idle") {
184
+ document.getElementById("ankh-guard-browser-handoff")?.remove();
185
+ schedule();
186
+ return;
187
+ }
188
+ if (result.state === "waiting") waitingOverlay();
189
+ if (result.state === "ready" && result.action === "reload" && typeof result.cutoverId !== "string") {
190
+ location.reload();
191
+ return;
192
+ }
193
+ if (result.state !== "ready" || typeof result.cutoverId !== "string") {
194
+ schedule(ACTIVE_RETRY_MS);
195
+ return;
196
+ }
197
+ if (result.action === "reload" && result.authentication === "existing-cookie") {
198
+ const returnPath = safeReturnPath(location.href, location.origin);
199
+ const nextPending = {
200
+ version: 1,
201
+ cutoverId: result.cutoverId,
202
+ channel: "original-tab",
203
+ authentication: "existing-cookie",
204
+ capability: tabCapability,
205
+ ...returnPath === void 0 ? {} : { returnPath }
206
+ };
207
+ pending = nextPending;
208
+ writePending(nextPending);
209
+ waitingOverlay();
210
+ location.reload();
211
+ return;
212
+ }
213
+ if (result.action === "replace" && result.authentication === "launch-url" && typeof result.launchUrl === "string") {
214
+ const launch = new URL(result.launchUrl);
215
+ if (launch.origin !== location.origin || launch.pathname !== "/" || launch.search === "" || launch.username !== "" || launch.password !== "" || launch.hash !== "") {
216
+ schedule(ACTIVE_RETRY_MS);
217
+ return;
218
+ }
219
+ const returnPath = safeReturnPath(location.href, location.origin);
220
+ const nextPending = {
221
+ version: 1,
222
+ cutoverId: result.cutoverId,
223
+ channel: "original-tab",
224
+ authentication: "launch-url",
225
+ capability: tabCapability,
226
+ ...returnPath === void 0 ? {} : { returnPath }
227
+ };
228
+ pending = nextPending;
229
+ writePending(nextPending);
230
+ waitingOverlay();
231
+ location.replace(launch.href);
232
+ return;
233
+ }
234
+ } catch {
235
+ noteDisconnected();
236
+ schedule(errorRetryMs);
237
+ errorRetryMs = Math.min(errorRetryMs * 2, ERROR_RETRY_MAX_MS);
238
+ return;
239
+ }
240
+ schedule(ACTIVE_RETRY_MS);
241
+ };
242
+ tick();
243
+ return () => {
244
+ disposed = true;
245
+ if (timer !== void 0) clearTimeout(timer);
246
+ };
247
+ }
248
+ //#endregion
249
+ exports.apply = apply;
250
+ exports.fallbackCutoverId = fallbackCutoverId;
251
+ exports.inject = inject;
252
+ exports.safeReturnPath = safeReturnPath;
253
+ return module.exports;
254
+ }
255
+ });
256
+
257
+ //# sourceMappingURL=client.js.map
package/lib/exit-agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as isDirectInvocation } from "./defaults-CduvOcGq.js";
2
- import { n as findPidOnPort } from "./processes-hCAmwma-.js";
2
+ import { m as findPidOnPort, u as registerCurrentTestProcess } from "./test-seam-dwvaKjRp.js";
3
3
  import { writeFileSync } from "node:fs";
4
4
  //#region lib/types/exit-agent.js
5
5
  /**
@@ -52,6 +52,9 @@ function exitAgentMain(env = process.env) {
52
52
  performExit(port, resultFile, env.WD_INITIATOR);
53
53
  }, delayMs);
54
54
  }
55
- if (isDirectInvocation(import.meta.url)) exitAgentMain();
55
+ if (isDirectInvocation(import.meta.url)) {
56
+ registerCurrentTestProcess();
57
+ exitAgentMain();
58
+ }
56
59
  //#endregion
57
60
  export { exitAgentMain, performExit };