@playcraft/cli 0.1.6 → 0.1.7

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.
@@ -2,6 +2,11 @@ import { connect as connectTcp } from 'node:net';
2
2
  import { WebSocket, WebSocketServer } from 'ws';
3
3
  import { desktopHealthSchema, desktopResourceName, runtimeError, } from '@playcraft/common/cloud-workspace/runtime';
4
4
  import { controlTokenMatches, extractControlToken } from './auth.js';
5
+ /**
6
+ * A takeover holds the desktop for this long, whether or not a page is watching, and ends on its
7
+ * own unless the user renews it.
8
+ */
9
+ const HOLD_MS = 20 * 60_000;
5
10
  const sources = new WeakMap();
6
11
  const hosts = new WeakMap();
7
12
  export function setDesktopSource(state, source) {
@@ -33,6 +38,9 @@ export class DesktopHost {
33
38
  sockets = new WebSocketServer({ noServer: true });
34
39
  source;
35
40
  upstream = loopbackUpstream(process.env.PLAYCRAFT_DESKTOP_UPSTREAM);
41
+ holdMs = holdDuration(process.env.PLAYCRAFT_DESKTOP_HOLD_MS);
42
+ holdExpiresAt;
43
+ holdTimer;
36
44
  constructor(state) {
37
45
  this.state = state;
38
46
  this.source = sources.get(state);
@@ -46,6 +54,9 @@ export class DesktopHost {
46
54
  streamGeneration: this.generation,
47
55
  holder: this.holder,
48
56
  agentInput: this.holder === 'human' ? 'revoked' : 'open',
57
+ ...(this.holder === 'human' && this.holdExpiresAt
58
+ ? { holdExpiresAt: new Date(this.holdExpiresAt).toISOString() }
59
+ : {}),
49
60
  });
50
61
  }
51
62
  async refresh() {
@@ -71,12 +82,21 @@ export class DesktopHost {
71
82
  }
72
83
  this.holder = 'human';
73
84
  this.cutViewers();
85
+ this.hold();
86
+ writeJson(response, 200, this.snapshot());
87
+ return;
88
+ }
89
+ if (request.method === 'POST' && url.pathname === '/runtime/desktop/renew') {
90
+ if (this.holder !== 'human') {
91
+ writeError(response, 'desktop_not_held');
92
+ return;
93
+ }
94
+ this.hold();
74
95
  writeJson(response, 200, this.snapshot());
75
96
  return;
76
97
  }
77
98
  if (request.method === 'DELETE' && url.pathname === '/runtime/desktop') {
78
- this.holder = 'none';
79
- this.cutViewers();
99
+ this.release();
80
100
  writeJson(response, 200, this.snapshot());
81
101
  return;
82
102
  }
@@ -114,9 +134,30 @@ export class DesktopHost {
114
134
  this.sockets.handleUpgrade(request, socket, head, (ws) => this.attach(ws, generation));
115
135
  }
116
136
  dispose() {
137
+ this.release();
138
+ this.sockets.close();
139
+ }
140
+ release() {
141
+ this.clearHold();
117
142
  this.holder = 'none';
118
143
  this.cutViewers();
119
- this.sockets.close();
144
+ }
145
+ /** Starts or renews the hold; when it runs out the desktop is released as if by hand. */
146
+ hold() {
147
+ this.clearHold();
148
+ this.holdExpiresAt = Date.now() + this.holdMs;
149
+ this.holdTimer = setTimeout(() => {
150
+ this.holdTimer = undefined;
151
+ if (this.holder === 'human')
152
+ this.release();
153
+ }, this.holdMs);
154
+ this.holdTimer.unref?.();
155
+ }
156
+ clearHold() {
157
+ if (this.holdTimer)
158
+ clearTimeout(this.holdTimer);
159
+ this.holdTimer = undefined;
160
+ this.holdExpiresAt = undefined;
120
161
  }
121
162
  currentStatus() {
122
163
  if (this.source)
@@ -248,6 +289,7 @@ export function desktopRoute(pathname) {
248
289
  return (pathname === '/runtime/desktop/health' ||
249
290
  pathname === '/runtime/desktop/connect' ||
250
291
  pathname === '/runtime/desktop/takeover' ||
292
+ pathname === '/runtime/desktop/renew' ||
251
293
  pathname === '/runtime/desktop' ||
252
294
  pathname.startsWith('/runtime/desktop/resources/'));
253
295
  }
@@ -268,6 +310,10 @@ function loopbackUpstream(value) {
268
310
  return undefined;
269
311
  return { port };
270
312
  }
313
+ function holdDuration(value) {
314
+ const parsed = Number(value);
315
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : HOLD_MS;
316
+ }
271
317
  function probePort(port) {
272
318
  return new Promise((resolve) => {
273
319
  const socket = connectTcp({ host: '127.0.0.1', port });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,8 +48,8 @@
48
48
  "@gltf-transform/core": "^4.3.0",
49
49
  "@gltf-transform/extensions": "^4.3.0",
50
50
  "@gltf-transform/functions": "^4.3.0",
51
- "@playcraft/build": "^0.1.6",
52
- "@playcraft/common": "^0.1.6",
51
+ "@playcraft/build": "^0.1.7",
52
+ "@playcraft/common": "^0.1.7",
53
53
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
54
54
  "chokidar": "^4.0.3",
55
55
  "commander": "^13.1.0",