beauticode-dsh 1.0.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 (40) hide show
  1. package/README.zh-CN.md +48 -0
  2. package/agent.mjs +595 -0
  3. package/atmosphere.js +306 -0
  4. package/bin/beauticode-dsh +219 -0
  5. package/cli.js +219 -0
  6. package/client.js +370 -0
  7. package/console.js +303 -0
  8. package/control-client.mjs +445 -0
  9. package/cordis.patch.yml +4 -0
  10. package/host-apply.mjs +163 -0
  11. package/index.mjs +570 -0
  12. package/package.json +56 -0
  13. package/presets.mjs +73 -0
  14. package/themes/internal-beyond/NOTICE.md +9 -0
  15. package/themes/internal-beyond/bg-canvas-4k.png +0 -0
  16. package/themes/internal-beyond/bg-canvas.png +0 -0
  17. package/themes/internal-beyond/bg-infernal.jpg +0 -0
  18. package/themes/internal-beyond/bg-internal.jpg +0 -0
  19. package/ui-host.mjs +292 -0
  20. package/vendor/adapter-dsh/bridge.js +215 -0
  21. package/vendor/adapter-dsh/host-descriptor.js +15 -0
  22. package/vendor/adapter-dsh/index.js +6 -0
  23. package/vendor/adapter-dsh/injector-lock.js +27 -0
  24. package/vendor/adapter-dsh/package.json +5 -0
  25. package/vendor/adapter-dsh/session.js +527 -0
  26. package/vendor/adapter-dsh/token.js +46 -0
  27. package/vendor/adapter-dsh/tray-handoff.js +54 -0
  28. package/vendor/core/apply-transaction.js +287 -0
  29. package/vendor/core/background-store.js +1116 -0
  30. package/vendor/core/bundled-gallery.js +53 -0
  31. package/vendor/core/constants.js +47 -0
  32. package/vendor/core/error-message.js +208 -0
  33. package/vendor/core/file-lock.js +151 -0
  34. package/vendor/core/host-session.js +2 -0
  35. package/vendor/core/index.js +12 -0
  36. package/vendor/core/media-server.js +544 -0
  37. package/vendor/core/media-validation.js +244 -0
  38. package/vendor/core/package.json +5 -0
  39. package/vendor/core/paths.js +168 -0
  40. package/vendor/core/types.js +15 -0
@@ -0,0 +1,527 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { ApplyTransaction, BackgroundStore, MediaServerController, buildHostApplyPayload, defaultDataRoot, resolveSessionBundledThemes, } from "../core/index.js";
4
+ import { DshHostApplier, normalizeDshBaseUrl } from "./bridge.js";
5
+ import { DSH_HOST_DESCRIPTOR } from "./host-descriptor.js";
6
+ import { acquireDshInjectorLock } from "./injector-lock.js";
7
+ import { ensureBridgeToken } from "./token.js";
8
+ import { trayHandoffRequested } from "./tray-handoff.js";
9
+ export class DshSession {
10
+ descriptor = DSH_HOST_DESCRIPTOR;
11
+ cdpPort = null;
12
+ dataRoot;
13
+ verifyDeadlineMs;
14
+ pollMs;
15
+ honorTrayHandoff;
16
+ baseUrl;
17
+ store;
18
+ media;
19
+ host = null;
20
+ releaseLock = null;
21
+ watchTimer = null;
22
+ handoffTimer = null;
23
+ watchTask = null;
24
+ stopTask = null;
25
+ activeOperations = new Set();
26
+ closed = false;
27
+ userBusy = false;
28
+ onError;
29
+ onStatus;
30
+ lastWatchError = "";
31
+ fishMode = false;
32
+ videoMuted = true;
33
+ backgroundTone = "auto";
34
+ activeThemeId = null;
35
+ lastProgressWriteAt = 0;
36
+ lastProgressWriteSec = -1;
37
+ progressWriteInFlight = false;
38
+ constructor(opts = {}) {
39
+ this.dataRoot = opts.dataRoot ?? defaultDataRoot();
40
+ this.verifyDeadlineMs = opts.verifyDeadlineMs ?? 30_000;
41
+ this.pollMs = opts.pollMs ?? 2_000;
42
+ this.honorTrayHandoff = opts.honorTrayHandoff !== false;
43
+ this.baseUrl = normalizeDshBaseUrl(opts.baseUrl ?? "http://127.0.0.1:3080");
44
+ this.store = new BackgroundStore({
45
+ root: this.dataRoot,
46
+ bundledThemes: resolveSessionBundledThemes({
47
+ enabled: opts.bundledGallery,
48
+ imagePath: opts.bundledGalleryImagePath,
49
+ searchRoots: [
50
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."),
51
+ process.cwd(),
52
+ ],
53
+ }),
54
+ });
55
+ this.media = new MediaServerController({
56
+ enabled: true,
57
+ trustedOrigins: [this.baseUrl.origin],
58
+ });
59
+ this.onError = opts.onError ?? null;
60
+ this.onStatus = opts.onStatus ?? null;
61
+ }
62
+ get isBusy() {
63
+ return this.userBusy;
64
+ }
65
+ get isOpen() {
66
+ return !this.closed && this.releaseLock != null;
67
+ }
68
+ get isHostReady() {
69
+ return (this.host?.activeSessionCount ?? 0) > 0;
70
+ }
71
+ async start() {
72
+ if (this.closed)
73
+ throw new Error("Session already stopped");
74
+ if (this.releaseLock)
75
+ throw new Error("Session already started");
76
+ await this.store.init();
77
+ this.releaseLock = await acquireDshInjectorLock(this.dataRoot);
78
+ try {
79
+ const token = await ensureBridgeToken(this.dataRoot);
80
+ this.host = new DshHostApplier({
81
+ baseUrl: this.baseUrl.href,
82
+ token,
83
+ pollMs: Math.min(250, Math.max(50, Math.floor(this.pollMs / 4))),
84
+ });
85
+ }
86
+ catch (error) {
87
+ await this.releaseLock().catch(() => { });
88
+ this.releaseLock = null;
89
+ throw error;
90
+ }
91
+ this.startWatchLoop();
92
+ if (this.honorTrayHandoff) {
93
+ this.startHandoffLoop();
94
+ void this.yieldToTrayIfRequested();
95
+ }
96
+ void this.watchOnce();
97
+ return { port: this.bridgePort() };
98
+ }
99
+ async apply(input) {
100
+ return this.trackOperation(this.applyInternal(input));
101
+ }
102
+ async applyInternal(input) {
103
+ if (!this.releaseLock || this.closed || !this.host) {
104
+ throw new Error("Session is not started");
105
+ }
106
+ if (this.userBusy) {
107
+ return {
108
+ ok: false,
109
+ error: "Another background apply is already in progress.",
110
+ rolledBack: false,
111
+ };
112
+ }
113
+ this.userBusy = true;
114
+ try {
115
+ const tx = new ApplyTransaction({
116
+ store: this.store,
117
+ media: this.media,
118
+ host: this.host,
119
+ verifyDeadlineMs: this.verifyDeadlineMs,
120
+ offline: false,
121
+ });
122
+ const result = await tx.run(input);
123
+ if (result.ok) {
124
+ this.activeThemeId = null;
125
+ this.lastProgressWriteSec = -1;
126
+ if (input.type === "clear") {
127
+ this.fishMode = false;
128
+ }
129
+ else if (this.fishMode) {
130
+ await this.host.setFishMode(true).catch(() => null);
131
+ }
132
+ if (!this.videoMuted || input.type === "video") {
133
+ await this.host.setMuted(this.videoMuted).catch(() => null);
134
+ }
135
+ if (input.type === "image" && input.effects?.preset === "infernal") {
136
+ this.backgroundTone = "dark";
137
+ }
138
+ else if (input.type === "image" && input.effects?.preset === "internal") {
139
+ this.backgroundTone = "light";
140
+ }
141
+ await this.host.setBackgroundTone(this.backgroundTone).catch(() => null);
142
+ }
143
+ return result;
144
+ }
145
+ finally {
146
+ this.userBusy = false;
147
+ }
148
+ }
149
+ async reapply() {
150
+ return this.trackOperation(this.reapplyInternal());
151
+ }
152
+ async reapplyInternal() {
153
+ if (!this.releaseLock || this.closed || !this.host) {
154
+ throw new Error("Session is not started");
155
+ }
156
+ if (this.userBusy) {
157
+ return {
158
+ ok: false,
159
+ error: "Another background apply is already in progress.",
160
+ rolledBack: false,
161
+ };
162
+ }
163
+ this.userBusy = true;
164
+ let stagedImage = null;
165
+ let stagedVideo = null;
166
+ try {
167
+ const manifest = await this.store.readActiveManifest();
168
+ let resumeAt = null;
169
+ if (manifest.background?.type === "video") {
170
+ try {
171
+ const position = await this.host.getPlaybackPosition();
172
+ if (position.ok && position.hasVideo && Number.isFinite(position.currentTime)) {
173
+ resumeAt = Math.max(0, position.currentTime);
174
+ }
175
+ }
176
+ catch {
177
+ /* Fall back to the bound saved theme below. */
178
+ }
179
+ if (resumeAt == null && this.activeThemeId) {
180
+ resumeAt = await this.store.getSavedThemeVideoPosition(this.activeThemeId);
181
+ }
182
+ }
183
+ if (manifest.background) {
184
+ stagedImage = await this.media.stage(path.join(this.store.paths.activeDir, manifest.background.image));
185
+ if (manifest.background.type === "video" && manifest.background.video) {
186
+ const runtimeVideoPath = await this.store.prepareRuntimeVideo(manifest);
187
+ if (!runtimeVideoPath) {
188
+ throw new Error("DSH video reapply requires a detached runtime copy.");
189
+ }
190
+ stagedVideo = await this.media.stage(runtimeVideoPath);
191
+ }
192
+ }
193
+ const staged = { image: stagedImage, video: stagedVideo };
194
+ const payload = await buildHostApplyPayload(this.store, manifest, staged, "");
195
+ if (payload.video && resumeAt != null)
196
+ payload.video.startAt = resumeAt;
197
+ await this.host.apply(payload);
198
+ const verify = await this.host.verify({
199
+ generation: manifest.generation,
200
+ media: manifest.background?.type ?? "clear",
201
+ }, { deadlineMs: this.verifyDeadlineMs });
202
+ if (verify.status !== "pass") {
203
+ await this.media.abort(stagedVideo);
204
+ await this.media.abort(stagedImage);
205
+ stagedVideo = null;
206
+ stagedImage = null;
207
+ return {
208
+ ok: false,
209
+ error: `Live verify did not pass (${verify.status}): ${verify.reason}`,
210
+ rolledBack: false,
211
+ };
212
+ }
213
+ await this.media.commit(staged);
214
+ stagedVideo = null;
215
+ stagedImage = null;
216
+ if (!manifest.background) {
217
+ this.fishMode = false;
218
+ this.activeThemeId = null;
219
+ }
220
+ else if (this.fishMode) {
221
+ await this.host.setFishMode(true).catch(() => null);
222
+ }
223
+ await this.host.setMuted(this.videoMuted).catch(() => null);
224
+ await this.host.setBackgroundTone(this.backgroundTone).catch(() => null);
225
+ return {
226
+ ok: true,
227
+ generation: manifest.generation,
228
+ mode: manifest.background?.type ?? "clear",
229
+ };
230
+ }
231
+ catch (error) {
232
+ await this.media.abort(stagedVideo);
233
+ await this.media.abort(stagedImage);
234
+ return {
235
+ ok: false,
236
+ error: error instanceof Error ? error.message : String(error),
237
+ rolledBack: false,
238
+ };
239
+ }
240
+ finally {
241
+ this.userBusy = false;
242
+ }
243
+ }
244
+ async status() {
245
+ const manifest = await this.store.readActiveManifest();
246
+ if (this.host)
247
+ await this.host.status().catch(() => null);
248
+ return {
249
+ host: this.descriptor,
250
+ port: this.bridgePort(),
251
+ sessions: this.host?.activeSessionCount ?? 0,
252
+ manifest,
253
+ mediaServer: this.media.activeImage?.url ?? this.media.activeVideo?.url ?? null,
254
+ fish: this.fishMode,
255
+ muted: this.videoMuted,
256
+ tone: this.backgroundTone,
257
+ };
258
+ }
259
+ async saveCurrentTheme(name) {
260
+ let videoPositionSec = null;
261
+ if (this.host) {
262
+ try {
263
+ const position = await this.host.getPlaybackPosition();
264
+ if (position.ok && position.hasVideo && Number.isFinite(position.currentTime)) {
265
+ videoPositionSec = position.currentTime;
266
+ }
267
+ }
268
+ catch {
269
+ /* Save without a resume position when no browser client is available. */
270
+ }
271
+ }
272
+ const theme = await this.store.saveCurrentTheme(name, { videoPositionSec });
273
+ if (theme.type === "video") {
274
+ this.activeThemeId = theme.id;
275
+ this.lastProgressWriteSec = -1;
276
+ }
277
+ else {
278
+ this.activeThemeId = null;
279
+ }
280
+ return theme;
281
+ }
282
+ async listSavedThemes() {
283
+ return this.store.listSavedThemes();
284
+ }
285
+ async deleteSavedTheme(themeId) {
286
+ const deleted = await this.store.deleteSavedTheme(themeId);
287
+ if (deleted && this.activeThemeId === themeId) {
288
+ this.activeThemeId = null;
289
+ this.lastProgressWriteSec = -1;
290
+ }
291
+ return deleted;
292
+ }
293
+ async useSavedTheme(themeId) {
294
+ try {
295
+ const saved = await this.store.loadSavedTheme(themeId);
296
+ const result = await this.apply(saved.input);
297
+ if (result.ok) {
298
+ this.activeThemeId = saved.input.type === "video" ? saved.themeId : null;
299
+ this.lastProgressWriteSec = -1;
300
+ }
301
+ return result;
302
+ }
303
+ catch (error) {
304
+ return {
305
+ ok: false,
306
+ error: error instanceof Error ? error.message : String(error),
307
+ rolledBack: false,
308
+ };
309
+ }
310
+ }
311
+ async setFishMode(enabled) {
312
+ if (!this.releaseLock || this.closed || !this.host) {
313
+ return { ok: false, fish: this.fishMode, sessions: 0, error: "Session is not started" };
314
+ }
315
+ const want = Boolean(enabled);
316
+ if (want) {
317
+ const manifest = await this.store.readActiveManifest();
318
+ if (!manifest.background) {
319
+ this.fishMode = false;
320
+ return {
321
+ ok: false,
322
+ fish: false,
323
+ sessions: 0,
324
+ error: "No active background. Apply an image or video first.",
325
+ };
326
+ }
327
+ }
328
+ this.fishMode = want;
329
+ try {
330
+ const result = await this.host.setFishMode(want);
331
+ if (result.ok)
332
+ this.fishMode = result.fish;
333
+ else if (!want)
334
+ this.fishMode = false;
335
+ return result;
336
+ }
337
+ catch (error) {
338
+ return {
339
+ ok: false,
340
+ fish: this.fishMode,
341
+ sessions: 0,
342
+ error: error instanceof Error ? error.message : String(error),
343
+ };
344
+ }
345
+ }
346
+ async setMuted(muted) {
347
+ if (!this.releaseLock || this.closed || !this.host) {
348
+ return {
349
+ ok: false,
350
+ muted: this.videoMuted,
351
+ blocked: false,
352
+ sessions: 0,
353
+ error: "Session is not started",
354
+ };
355
+ }
356
+ this.videoMuted = Boolean(muted);
357
+ try {
358
+ const result = await this.host.setMuted(this.videoMuted);
359
+ if (result.ok)
360
+ this.videoMuted = result.muted;
361
+ return result;
362
+ }
363
+ catch (error) {
364
+ return {
365
+ ok: false,
366
+ muted: this.videoMuted,
367
+ blocked: false,
368
+ sessions: 0,
369
+ error: error instanceof Error ? error.message : String(error),
370
+ };
371
+ }
372
+ }
373
+ async setBackgroundTone(tone) {
374
+ if (!this.releaseLock || this.closed || !this.host) {
375
+ return { ok: false, tone: this.backgroundTone, sessions: 0, error: "Session is not started" };
376
+ }
377
+ this.backgroundTone = tone === "light" || tone === "auto" ? tone : "dark";
378
+ try {
379
+ const result = await this.host.setBackgroundTone(this.backgroundTone);
380
+ if (result.ok)
381
+ this.backgroundTone = result.tone;
382
+ return result;
383
+ }
384
+ catch (error) {
385
+ return {
386
+ ok: false,
387
+ tone: this.backgroundTone,
388
+ sessions: 0,
389
+ error: error instanceof Error ? error.message : String(error),
390
+ };
391
+ }
392
+ }
393
+ async stop() {
394
+ if (this.stopTask)
395
+ return this.stopTask;
396
+ this.stopTask = this.stopExclusive();
397
+ return this.stopTask;
398
+ }
399
+ async stopExclusive() {
400
+ this.closed = true;
401
+ if (this.watchTimer)
402
+ clearInterval(this.watchTimer);
403
+ this.watchTimer = null;
404
+ if (this.handoffTimer)
405
+ clearInterval(this.handoffTimer);
406
+ this.handoffTimer = null;
407
+ await Promise.allSettled([this.watchTask, ...this.activeOperations].filter((value) => Boolean(value)));
408
+ if (this.fishMode && this.host) {
409
+ await this.host.setFishMode(false).catch(() => null);
410
+ this.fishMode = false;
411
+ }
412
+ await this.media.close().catch(() => { });
413
+ if (this.releaseLock)
414
+ await this.releaseLock().catch(() => { });
415
+ this.releaseLock = null;
416
+ this.host = null;
417
+ }
418
+ trackOperation(operation) {
419
+ this.activeOperations.add(operation);
420
+ void operation.finally(() => this.activeOperations.delete(operation)).catch(() => { });
421
+ return operation;
422
+ }
423
+ bridgePort() {
424
+ if (this.baseUrl.port)
425
+ return Number(this.baseUrl.port);
426
+ return 80;
427
+ }
428
+ startWatchLoop() {
429
+ this.watchTimer = setInterval(() => void this.watchOnce(), this.pollMs);
430
+ this.watchTimer.unref?.();
431
+ }
432
+ startHandoffLoop() {
433
+ this.handoffTimer = setInterval(() => void this.yieldToTrayIfRequested(), 250);
434
+ this.handoffTimer.unref?.();
435
+ }
436
+ async yieldToTrayIfRequested() {
437
+ if (this.closed || this.stopTask)
438
+ return;
439
+ if (!(await trayHandoffRequested(this.dataRoot)))
440
+ return;
441
+ this.onStatus?.("beautiCode 托盘正在接管,正在释放本机会话。");
442
+ await this.stop();
443
+ }
444
+ async watchOnce() {
445
+ if (this.closed || !this.host || this.watchTask)
446
+ return;
447
+ const task = (async () => {
448
+ try {
449
+ const [status, manifest] = await Promise.all([
450
+ this.host.status(),
451
+ this.store.readActiveManifest(),
452
+ ]);
453
+ this.lastWatchError = "";
454
+ const media = manifest.background?.type ?? "clear";
455
+ // Media URLs are process-local. After the tray/session host restarts,
456
+ // the DSH page may still report the same generation while pointing at
457
+ // a dead loopback server, so force one reapply when local handles are
458
+ // absent.
459
+ const localMediaMissing = media === "image"
460
+ ? this.media.activeImage == null
461
+ : media === "video"
462
+ ? this.media.activeImage == null || this.media.activeVideo == null
463
+ : false;
464
+ const stale = status.connectedClients > 0 &&
465
+ (status.current?.generation !== manifest.generation ||
466
+ status.current.media !== media ||
467
+ localMediaMissing);
468
+ if (stale && !this.userBusy) {
469
+ this.onStatus?.("DeepSeek Harness 已连接,正在恢复当前背景。");
470
+ const result = await this.reapply();
471
+ if (!result.ok)
472
+ throw new Error(result.error);
473
+ }
474
+ await this.persistBoundThemeProgress();
475
+ }
476
+ catch (error) {
477
+ const err = error instanceof Error ? error : new Error(String(error));
478
+ if (err.message !== this.lastWatchError) {
479
+ this.lastWatchError = err.message;
480
+ this.onError?.(err);
481
+ }
482
+ }
483
+ })();
484
+ this.watchTask = task;
485
+ await task.finally(() => {
486
+ if (this.watchTask === task)
487
+ this.watchTask = null;
488
+ });
489
+ }
490
+ async persistBoundThemeProgress() {
491
+ if (!this.activeThemeId || !this.host || this.progressWriteInFlight || this.userBusy) {
492
+ return;
493
+ }
494
+ const now = Date.now();
495
+ if (now - this.lastProgressWriteAt < 2_000)
496
+ return;
497
+ this.progressWriteInFlight = true;
498
+ try {
499
+ const position = await this.host.getPlaybackPosition();
500
+ if (!position.ok || !position.hasVideo)
501
+ return;
502
+ let seconds = Number(position.currentTime);
503
+ if (!Number.isFinite(seconds) || seconds < 0)
504
+ return;
505
+ if (position.duration > 0 && seconds >= position.duration - 0.25)
506
+ seconds = 0;
507
+ if (this.lastProgressWriteSec >= 0 &&
508
+ Math.abs(seconds - this.lastProgressWriteSec) < 0.5 &&
509
+ !(seconds === 0 && this.lastProgressWriteSec !== 0)) {
510
+ this.lastProgressWriteAt = now;
511
+ return;
512
+ }
513
+ const result = await this.store.updateSavedThemeVideoPosition(this.activeThemeId, seconds);
514
+ if (result.ok) {
515
+ this.lastProgressWriteAt = now;
516
+ this.lastProgressWriteSec = result.positionSec ?? seconds;
517
+ }
518
+ else if (result.error === "Saved theme not found.") {
519
+ this.activeThemeId = null;
520
+ }
521
+ }
522
+ finally {
523
+ this.progressWriteInFlight = false;
524
+ }
525
+ }
526
+ }
527
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,46 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ export const DSH_BRIDGE_TOKEN_FILE = "dsh-bridge.token";
5
+ const TOKEN_PATTERN = /^[a-f0-9]{64}$/;
6
+ export function bridgeTokenPath(dataRoot) {
7
+ return path.join(dataRoot, DSH_BRIDGE_TOKEN_FILE);
8
+ }
9
+ async function readToken(file) {
10
+ const token = (await fs.readFile(file, "utf8")).trim();
11
+ if (!TOKEN_PATTERN.test(token)) {
12
+ throw new Error("DeepSeek Harness bridge token file is invalid.");
13
+ }
14
+ return token;
15
+ }
16
+ /** Create once with owner-only permissions where the platform supports them. */
17
+ export async function ensureBridgeToken(dataRoot) {
18
+ await fs.mkdir(dataRoot, { recursive: true });
19
+ const file = bridgeTokenPath(dataRoot);
20
+ try {
21
+ return await readToken(file);
22
+ }
23
+ catch (error) {
24
+ if (error?.code !== "ENOENT")
25
+ throw error;
26
+ }
27
+ const token = crypto.randomBytes(32).toString("hex");
28
+ try {
29
+ const handle = await fs.open(file, "wx", 0o600);
30
+ try {
31
+ await handle.writeFile(`${token}\n`, "utf8");
32
+ await handle.sync();
33
+ }
34
+ finally {
35
+ await handle.close();
36
+ }
37
+ return token;
38
+ }
39
+ catch (error) {
40
+ if (error?.code === "EEXIST") {
41
+ return readToken(file);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const TRAY_CLAIM_FILE = "tray-claim.json";
4
+ export const TRAY_CLAIM_SCHEMA = "beauticode.tray-claim/v1";
5
+ export const SESSION_HOST_FILE = "session-host.json";
6
+ export const SESSION_HOST_SCHEMA = "beauticode.session-host/v1";
7
+ export const DSH_CONTROL_FILE = "dsh-control.json";
8
+ export const DSH_CONTROL_SCHEMA = "beauticode.dsh-control/v1";
9
+ function isPidAlive(pid) {
10
+ if (!Number.isInteger(pid) || pid <= 0)
11
+ return false;
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
18
+ }
19
+ }
20
+ async function readLivePid(filePath, schema) {
21
+ let raw;
22
+ try {
23
+ raw = await fs.readFile(filePath, "utf8");
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (schema && parsed.schema !== schema)
31
+ return null;
32
+ const pid = Number(parsed.pid);
33
+ if (!Number.isInteger(pid) || pid <= 0 || !isPidAlive(pid))
34
+ return null;
35
+ return pid;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /** True when the tray is starting or a different session-host owns the data root. */
42
+ export async function trayHandoffRequested(dataRoot, selfPid = process.pid) {
43
+ const root = path.resolve(dataRoot);
44
+ const claimPid = await readLivePid(path.join(root, TRAY_CLAIM_FILE), TRAY_CLAIM_SCHEMA);
45
+ // In-process sessions never write this file; any live claim means the tray wants the lock.
46
+ if (claimPid != null)
47
+ return true;
48
+ const hostPid = await readLivePid(path.join(root, SESSION_HOST_FILE), SESSION_HOST_SCHEMA);
49
+ if (hostPid != null && hostPid !== selfPid)
50
+ return true;
51
+ const controlPid = await readLivePid(path.join(root, DSH_CONTROL_FILE), DSH_CONTROL_SCHEMA);
52
+ return controlPid != null && controlPid !== selfPid;
53
+ }
54
+ //# sourceMappingURL=tray-handoff.js.map