@platless/axom-cli 0.5.9 → 0.6.36

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 (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { decodeDesktopManagementFrame, parseDesktopJson, DesktopProtocolError } from '../desktopControl/protocol.js';
1
2
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableString, isNumber, isObject, isString, } from './common.js';
2
3
  export const MANAGEMENT_WIRE_MIN_VERSION = 1;
3
4
  /** Adds the `DRAINING` value to ClientUpdatePhase. */
@@ -12,7 +13,8 @@ export const MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION = 6;
12
13
  export const MANAGEMENT_WIRE_GPU_VERSION = 7;
13
14
  /** Adds `keepers` to heartbeats and the `keeper_control` / `keeper_control_result` pair (spec §8.1, Task 26). */
14
15
  export const MANAGEMENT_WIRE_KEEPERS_VERSION = 8;
15
- export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_KEEPERS_VERSION;
16
+ export const MANAGEMENT_WIRE_DESKTOP_VERSION = 9;
17
+ export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_DESKTOP_VERSION;
16
18
  /** Bounds of one heartbeat's `keepers` (spec §8.1). */
17
19
  export const KEEPER_STATUS_MAX_ITEMS = 32;
18
20
  export const KEEPER_LABEL_MAX_CHARS = 64;
@@ -137,9 +139,11 @@ function isManagementFrame(value) {
137
139
  playgroundCwd: isString,
138
140
  protocols: (protocols) => isProtocolRangeMap(protocols, false),
139
141
  os: isNullable(isOsInfo),
142
+ toolchain: (toolchain) => isObject(toolchain) && hasRequired(toolchain, { python: isNullableString, node: isNullableString }),
140
143
  update: isNullable(isUpdateStatus),
141
144
  metrics: isNullable(isWorkstationMetrics),
142
145
  keepers: isNullable((keepers) => Array.isArray(keepers) && keepers.every(isKeeperStatus)),
146
+ capabilitiesRevision: value => Number.isSafeInteger(value) && value > 0,
143
147
  });
144
148
  case 'heartbeat_ack': return hasRequired(value, { seq: isNumber });
145
149
  case 'channel_ticket_request': return hasRequired(value, { channel: isChannelKind });
@@ -157,8 +161,93 @@ function isManagementFrame(value) {
157
161
  }
158
162
  }
159
163
  export function encodeManagementFrame(frame) {
160
- return JSON.stringify(frame);
161
- }
162
- export function decodeManagementFrame(text) {
164
+ const text = JSON.stringify(frame);
165
+ if (isDesktopManagementType(frame.t))
166
+ decodeDesktopManagementFrame(text);
167
+ return text;
168
+ }
169
+ export function decodeManagementFrame(source) {
170
+ const text = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
171
+ // Desktop extensions get the strict lexical/shape guard before any lossy object parsing.
172
+ const parsed = JSON.parse(text);
173
+ if (isObject(parsed) && isDesktopManagementType(parsed.t))
174
+ return decodeDesktopManagementFrame(text);
175
+ if (isObject(parsed) && parsed.t === 'heartbeat' && Object.hasOwn(parsed, 'capabilitiesRevision'))
176
+ validateHeartbeatRevision(text);
163
177
  return decodeChannelFrame(text, DISCRIMINATORS, isManagementFrame);
164
178
  }
179
+ /** Select only the new root field. JSON.parse already validates syntax; legacy nesting, size,
180
+ * unknown fields and duplicate legacy keys keep their established decoder behavior. */
181
+ function validateHeartbeatRevision(text) {
182
+ let depth = 0, count = 0;
183
+ for (let offset = 0; offset < text.length; offset++) {
184
+ const char = text[offset];
185
+ if (char === '"') {
186
+ const start = offset++;
187
+ while (offset < text.length) {
188
+ if (text[offset] === '\\')
189
+ offset += 2;
190
+ else if (text[offset++] === '"')
191
+ break;
192
+ }
193
+ const end = offset--;
194
+ if (depth !== 1 || JSON.parse(text.slice(start, end)) !== 'capabilitiesRevision')
195
+ continue;
196
+ let tokenStart = end;
197
+ while (/\s/.test(text[tokenStart] ?? ''))
198
+ tokenStart++;
199
+ if (text[tokenStart] !== ':')
200
+ continue; // This string was a legacy field's value.
201
+ let tokenEnd = ++tokenStart;
202
+ while (tokenEnd < text.length && text[tokenEnd] !== ',' && text[tokenEnd] !== '}')
203
+ tokenEnd++;
204
+ const token = text.slice(tokenStart, tokenEnd).trim();
205
+ if (++count !== 1 || !/^[1-9][0-9]*$/.test(token) || !Number.isSafeInteger(Number(token))) {
206
+ throw new DesktopProtocolError('Invalid capabilities revision');
207
+ }
208
+ parseDesktopJson(`{"capabilitiesRevision":${token}}`);
209
+ }
210
+ else if (char === '{' || char === '[')
211
+ depth++;
212
+ else if (char === '}' || char === ']')
213
+ depth--;
214
+ }
215
+ }
216
+ function isDesktopManagementType(type) {
217
+ return typeof type === 'string' && ['desktop_background', 'desktop_lobby', 'desktop_lobby_result', 'workstation_capabilities', 'workstation_capabilities_request', 'desktop_prepare',
218
+ 'desktop_prepare_result', 'desktop_cancel', 'desktop_session_state'].includes(type);
219
+ }
220
+ export class ManagementFrameAdmission {
221
+ isCurrent;
222
+ ready;
223
+ disposed = false;
224
+ constructor(isCurrent = () => true) {
225
+ this.isCurrent = isCurrent;
226
+ }
227
+ admit(frame) {
228
+ if (this.disposed || !this.isCurrent())
229
+ return false;
230
+ if (frame.t === 'management_ready') {
231
+ if (this.ready || !Number.isInteger(frame.protocols.MANAGEMENT) || frame.protocols.MANAGEMENT < MANAGEMENT_WIRE_MIN_VERSION
232
+ || frame.protocols.MANAGEMENT > MANAGEMENT_WIRE_VERSION || !frame.generation)
233
+ return false;
234
+ this.ready = frame;
235
+ return true;
236
+ }
237
+ if (frame.t === 'heartbeat' && frame.capabilitiesRevision !== undefined)
238
+ return false;
239
+ if (!isDesktopManagementType(frame.t))
240
+ return true;
241
+ if (!this.ready || this.ready.protocols.MANAGEMENT < MANAGEMENT_WIRE_DESKTOP_VERSION)
242
+ return false;
243
+ switch (frame.t) {
244
+ case 'workstation_capabilities_request': return frame.generation === this.ready.generation;
245
+ case 'desktop_lobby':
246
+ case 'desktop_background':
247
+ case 'desktop_prepare': return frame.deviceGeneration === this.ready.generation;
248
+ case 'desktop_cancel': return true; // Current socket binding; T10 additionally fences its session/control generation.
249
+ default: return false; // Device-authored reports/results/states cannot be echoed as server activity.
250
+ }
251
+ }
252
+ dispose() { this.disposed = true; }
253
+ }
@@ -0,0 +1,15 @@
1
+ export function isRuntimeDiagnostics(value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3
+ return false;
4
+ const input = value;
5
+ const bounded = (value, max) => typeof value === 'string' &&
6
+ value.trim().length > 0 && value.length <= max && !/[\x00-\x1f\x7f]/.test(value);
7
+ if (!bounded(input.generation, 256) || !Number.isSafeInteger(input.revision) ||
8
+ input.revision < 0 || typeof input.available !== 'boolean' ||
9
+ !input.launch || typeof input.launch !== 'object')
10
+ return false;
11
+ const launch = input.launch;
12
+ return ['command-override', 'registry', 'registry-fallback', 'direct'].includes(launch.source) &&
13
+ bounded(launch.executable, 4096) && bounded(launch.platform, 64) && bounded(launch.architecture, 64) &&
14
+ launch.transport === 'stdio' && (input.profile === undefined || (typeof input.profile === 'object' && input.profile !== null && !Array.isArray(input.profile)));
15
+ }
@@ -0,0 +1,119 @@
1
+ import { promises as fs, constants } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { gunzipSync } from 'node:zlib';
5
+ import { z } from 'zod';
6
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
7
+ import { downloadVerified } from '../update/artifact.js';
8
+ import { privateDirectory } from '../keepers/kit/paths.js';
9
+ const legacyRdFiles = ['bin/axom-desktop-keeper', 'bin/axom-input-guardian', 'bin/axom-quic', 'xorg.conf'];
10
+ export const rdFiles = [...legacyRdFiles, 'bin/axom-display', 'bin/axom-desktop-session'];
11
+ const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
12
+ export const rdArtifactSchema = z.object({ version: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}$/),
13
+ filename: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]*\.(?:tar\.gz|tgz)$/),
14
+ url: z.string(), size: z.number().int().min(1).max(134217728), sha512: z.string().regex(/^[a-f0-9]{128}$/),
15
+ unpackedSize: z.number().int().min(1).max(268435456), ipcVersion: z.union([z.literal(1), z.literal(2)]),
16
+ files: z.record(z.string(), sha256),
17
+ }).refine(artifact => {
18
+ const files = artifact.ipcVersion === 1 ? legacyRdFiles : rdFiles;
19
+ const licensed = artifact.ipcVersion === 2 && 'opus-LICENSE' in artifact.files;
20
+ return Object.keys(artifact.files).length === files.length + (licensed ? 1 : 0) && files.every(file => file in artifact.files);
21
+ }, 'RD artifact files do not match its runtime version');
22
+ export function rdPlatform(osRelease, architecture) {
23
+ const values = Object.fromEntries(osRelease.split('\n').map(line => /^([A-Z_]+)=["']?([^"']*)["']?$/.exec(line)).filter(Boolean).map(match => [match[1], match[2]]));
24
+ if (values.ID !== 'debian' || values.VERSION_ID !== '13' || architecture !== 'x64') {
25
+ throw new Error('Remote desktop currently supports Debian 13 amd64; this distribution/architecture has no qualified RD package');
26
+ }
27
+ return 'linux-x64-debian-13';
28
+ }
29
+ export function unpackRd(bytes, artifact) {
30
+ const archive = gunzipSync(bytes, { maxOutputLength: artifact.unpackedSize + 65536 });
31
+ const files = new Map();
32
+ let unpackedSize = 0;
33
+ let terminated = false;
34
+ for (let offset = 0; offset + 512 <= archive.length;) {
35
+ const header = archive.subarray(offset, offset + 512);
36
+ if (header.every(byte => byte === 0)) {
37
+ if (offset + 1024 > archive.length || archive.subarray(offset).some(byte => byte !== 0))
38
+ throw new Error('RD archive has an invalid terminator or trailing records');
39
+ terminated = true;
40
+ break;
41
+ }
42
+ const field = (start, length) => header.subarray(start, start + length).toString('utf8').replace(/\0.*$/s, '');
43
+ const name = field(0, 100), prefix = field(345, 155), sizeText = field(124, 12).trim();
44
+ const checksum = field(148, 8).trim();
45
+ const actualChecksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
46
+ if (field(257, 6) !== 'ustar' || field(263, 2) !== '00' || !/^[0-7]+$/.test(checksum) || Number.parseInt(checksum, 8) !== actualChecksum)
47
+ throw new Error('RD archive has an invalid USTAR header');
48
+ if (prefix || !/^[0-7]+$/.test(sizeText) || !Object.hasOwn(artifact.files, name) || files.has(name) || ![0, 48].includes(header[156])) {
49
+ throw new Error('RD archive contains an unexpected file, path, link or duplicate');
50
+ }
51
+ const size = Number.parseInt(sizeText, 8);
52
+ offset += 512;
53
+ unpackedSize += size;
54
+ if (!Number.isSafeInteger(size) || offset + size > archive.length || unpackedSize > artifact.unpackedSize)
55
+ throw new Error('RD archive exceeds its declared size');
56
+ const contents = archive.subarray(offset, offset + size);
57
+ if (createHash('sha256').update(contents).digest('hex') !== artifact.files[name])
58
+ throw new Error(`RD component checksum mismatch: ${name}`);
59
+ files.set(name, contents);
60
+ offset += Math.ceil(size / 512) * 512;
61
+ }
62
+ if (!terminated || files.size !== Object.keys(artifact.files).length || unpackedSize !== artifact.unpackedSize)
63
+ throw new Error('RD archive is incomplete');
64
+ return files;
65
+ }
66
+ export async function verifyRdInstallation(directory, artifact) {
67
+ for (const name of Object.keys(rdArtifactSchema.parse(artifact).files)) {
68
+ const file = await fs.open(path.join(directory, name), constants.O_RDONLY | constants.O_NOFOLLOW);
69
+ try {
70
+ const info = await file.stat();
71
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.mode & 0o022 || info.size > artifact.unpackedSize)
72
+ throw new Error('Unsafe RD installation');
73
+ if (createHash('sha256').update(await file.readFile()).digest('hex') !== artifact.files[name])
74
+ throw new Error(`RD component checksum mismatch: ${name}`);
75
+ }
76
+ finally {
77
+ await file.close();
78
+ }
79
+ }
80
+ }
81
+ export async function installRd(directory, server, platform, fetchImpl = globalThis.fetch) {
82
+ const response = await fetchImpl(deploymentDownloadURL(server, '/download/client/desktop/manifest.json'), { redirect: 'error', signal: AbortSignal.timeout(15000) });
83
+ if (!response.ok)
84
+ throw new Error(`The server does not publish Linux RD packages (HTTP ${response.status})`);
85
+ const text = await response.text();
86
+ if (Buffer.byteLength(text) > 1048576)
87
+ throw new Error('RD catalog is too large');
88
+ const catalog = z.object({ schemaVersion: z.literal(1), artifacts: z.record(z.string(), rdArtifactSchema) }).parse(JSON.parse(text));
89
+ const artifact = catalog.artifacts[platform];
90
+ if (!artifact)
91
+ throw new Error(`The server has no RD package for ${platform}`);
92
+ if (artifact.url !== `/download/client/desktop/${artifact.filename}`)
93
+ throw new Error('RD artifact URL does not match its filename');
94
+ const target = path.join(directory, 'rd', 'versions', artifact.sha512.slice(0, 32));
95
+ try {
96
+ await verifyRdInstallation(target, artifact);
97
+ return { directory: target, artifact };
98
+ }
99
+ catch (error) {
100
+ if (error.code !== 'ENOENT')
101
+ throw error;
102
+ }
103
+ const downloaded = await downloadVerified(artifact, { server, fetch: fetchImpl });
104
+ await privateDirectory(path.dirname(target));
105
+ const staging = await fs.mkdtemp(path.join(path.dirname(target), '.install-'));
106
+ try {
107
+ const files = unpackRd(await fs.readFile(downloaded.path), artifact);
108
+ await privateDirectory(path.join(staging, 'bin'));
109
+ for (const [name, contents] of files)
110
+ await fs.writeFile(path.join(staging, name), contents, { flag: 'wx', mode: name.startsWith('bin/') ? 0o700 : 0o600 });
111
+ await verifyRdInstallation(staging, artifact);
112
+ await fs.rename(staging, target);
113
+ return { directory: target, artifact };
114
+ }
115
+ finally {
116
+ await downloaded.cleanup();
117
+ await fs.rm(staging, { recursive: true, force: true });
118
+ }
119
+ }
@@ -0,0 +1,119 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { z } from 'zod';
6
+ import { loadConfig, resolveServer } from '../../config.js';
7
+ import { loadClientConfig } from '../config.js';
8
+ import { normalizeWorkstationServer } from '../control/serverIdentity.js';
9
+ import { withPrivateLock } from '../control/locks.js';
10
+ import { installRd, rdPlatform } from './artifact.js';
11
+ import { readRdState, writeRdState } from './state.js';
12
+ import { chooseSession, discoverSessions } from './sessions.js';
13
+ const execute = promisify(execFile);
14
+ export function rdSessionSelection(previous, options) {
15
+ const session = z.enum(['auto', 'existing', 'virtual']).parse(options.rdSession ?? previous?.session ?? 'auto');
16
+ const display = options.rdDisplay ?? (options.rdSession === undefined ? previous?.display : undefined);
17
+ if (display !== undefined && !/^:\d+(?:\.\d+)?$/.test(display))
18
+ throw new Error('--rd-display must be a local X11 display such as :10');
19
+ if (session === 'virtual' && display)
20
+ throw new Error('--rd-display selects an existing session and cannot be combined with --rd-session virtual');
21
+ return { session, display };
22
+ }
23
+ export async function configureRd(directory, options) {
24
+ if (!options.enableRd && !options.disableRd && options.rdSession === undefined && options.rdDisplay === undefined)
25
+ return;
26
+ if (process.platform !== 'linux' || process.getuid?.() === 0)
27
+ throw new Error('Linux RD must be configured and run as the workstation user, not root');
28
+ if (options.enableRd && options.disableRd)
29
+ throw new Error('--enable-rd and --disable-rd cannot be combined');
30
+ if (!options.enableRd && !options.disableRd)
31
+ throw new Error('--rd-session and --rd-display require --enable-rd');
32
+ await withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
33
+ const previous = await readRdState(directory);
34
+ const registered = await loadClientConfig(directory).catch(error => {
35
+ if (error.code !== 'ENOENT')
36
+ throw error;
37
+ return undefined;
38
+ });
39
+ const server = normalizeWorkstationServer(options.server ?? registered?.server ?? resolveServer(await loadConfig()));
40
+ if (options.disableRd) {
41
+ await writeRdState(directory, previous ? { ...previous, enabled: false } : { schemaVersion: 1, enabled: false, server, session: 'auto' });
42
+ return;
43
+ }
44
+ if (registered && normalizeWorkstationServer(registered.server) !== server)
45
+ throw new Error('This workstation is already registered to another server; change its registration before enabling RD');
46
+ const { session, display } = rdSessionSelection(previous, options);
47
+ const platform = rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
48
+ const installed = await installRd(directory, server, platform);
49
+ await ensureDependencies(installed.directory, session, display, installed.artifact);
50
+ await writeRdState(directory, { schemaVersion: 1, enabled: true, server, session, ...(display ? { display } : {}),
51
+ installation: installed.directory, artifact: installed.artifact });
52
+ console.log(`Remote desktop enabled (${session}, ${installed.artifact.version}); capture starts only when an authorized viewer connects`);
53
+ });
54
+ }
55
+ async function ensureDependencies(installation, session, display, artifact) {
56
+ let runtimeMissing = false;
57
+ try {
58
+ await execute(path.join(installation, 'bin/axom-desktop-keeper'), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
59
+ }
60
+ catch {
61
+ runtimeMissing = true;
62
+ }
63
+ const selected = chooseSession(await discoverSessions(), { session, display });
64
+ const packages = runtimeMissing ? ['libavcodec61', 'libavutil59', 'libswscale8', 'libgstreamer-plugins-bad1.0-0', 'libx11-6', 'libxext6', 'libxdamage1', 'libxfixes3', 'libxtst6', 'libxrandr2', 'libxcomposite1', 'zlib1g'] : [];
65
+ if (artifact?.ipcVersion === 2)
66
+ packages.push('python3', 'x11-xserver-utils', 'x11-utils', 'xauth', 'libpulse0', 'pulseaudio', 'pulseaudio-utils', 'libasound2-plugins');
67
+ if (!selected)
68
+ packages.push('xserver-xorg-core', 'xserver-xorg-video-dummy', 'xfce4-session', 'xfwm4', 'xfce4-panel', 'xfdesktop4', 'xfce4-settings', 'thunar', 'xfce4-terminal', 'dbus-x11', 'xauth', 'x11-utils', 'fonts-dejavu-core');
69
+ if (!packages.length)
70
+ return;
71
+ const missing = [];
72
+ for (const name of packages) {
73
+ try {
74
+ const { stdout } = await execute('dpkg-query', ['-W', '-f=${db:Status-Status}', name], { timeout: 2000, maxBuffer: 4096 });
75
+ if (stdout.trim() !== 'installed')
76
+ missing.push(name);
77
+ }
78
+ catch {
79
+ missing.push(name);
80
+ }
81
+ }
82
+ if (missing.length) {
83
+ console.log(`Installing remote desktop dependencies: ${missing.join(', ')}`);
84
+ try {
85
+ await execute('sudo', ['-n', 'apt-get', 'install', '-y', '--no-install-recommends', ...missing], { timeout: 180000, maxBuffer: 1048576 });
86
+ }
87
+ catch {
88
+ throw new Error(`Install the required RD packages, then retry: sudo apt-get install --no-install-recommends ${missing.join(' ')}`);
89
+ }
90
+ }
91
+ await execute(path.join(installation, 'bin/axom-desktop-keeper'), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
92
+ if (artifact?.ipcVersion === 2) {
93
+ for (const command of ['axom-display', 'axom-desktop-session'])
94
+ await execute(path.join(installation, 'bin', command), ['--self-test'], { timeout: 5000, maxBuffer: 8192 });
95
+ }
96
+ }
97
+ export async function rdStatus(directory) {
98
+ const state = await readRdState(directory);
99
+ if (!state)
100
+ return { enabled: false, installed: false };
101
+ let observed;
102
+ let update;
103
+ try {
104
+ observed = JSON.parse(await fs.readFile(path.join(directory, 'rd', 'status.json'), 'utf8'));
105
+ }
106
+ catch (error) {
107
+ if (error.code !== 'ENOENT')
108
+ throw error;
109
+ }
110
+ try {
111
+ update = JSON.parse(await fs.readFile(path.join(directory, 'rd', 'update-status.json'), 'utf8'));
112
+ }
113
+ catch (error) {
114
+ if (error.code !== 'ENOENT')
115
+ throw error;
116
+ }
117
+ return { enabled: state.enabled, installed: state.installation !== undefined, version: state.artifact?.version, selection: state.session, server: state.server,
118
+ ...(state.display ? { requestedDisplay: state.display } : {}), ...(state.enabled ? { observed, update } : {}) };
119
+ }
@@ -0,0 +1,29 @@
1
+ import { captureSamplingModes } from '../desktopControl/captureModes.js';
2
+ export function parseXrandr(text) {
3
+ const screen = /current (\d+) x (\d+)/.exec(text);
4
+ const outputs = [...text.matchAll(/^(\S+) connected(?: primary)? (\d+)x(\d+)\+(\d+)\+(\d+).*$/gm)];
5
+ if (!screen || outputs.length !== 1 || outputs[0][4] !== '0' || outputs[0][5] !== '0' ||
6
+ screen[1] !== outputs[0][2] || screen[2] !== outputs[0][3])
7
+ throw new Error('No consistent single-output X11 desktop is available');
8
+ const output = outputs[0];
9
+ const section = text.slice(output.index + output[0].length).split(/^\S/m)[0];
10
+ const modes = [...section.matchAll(/^\s+(\d+)x(\d+)(?:\S*)\s/gm)]
11
+ .map(match => ({ width: Number(match[1]), height: Number(match[2]) }))
12
+ .filter(mode => mode.width >= 2 && mode.height >= 2 && mode.width <= 4096 && mode.height <= 4096 && mode.width % 2 === 0 && mode.height % 2 === 0);
13
+ return { output: output[1], width: Number(output[2]), height: Number(output[3]),
14
+ modes: modes.filter((mode, index) => modes.findIndex(other => other.width === mode.width && other.height === mode.height) === index) };
15
+ }
16
+ export function desktopDisplayControl(target, actual, managed) {
17
+ if (target.kind !== 'window' && target.width === actual.width && target.height === actual.height &&
18
+ target.width % 2 === 0 && target.height % 2 === 0 && !/^rdp\d+$/i.test(actual.output)) {
19
+ if (managed && /^DUMMY\d+$/i.test(actual.output))
20
+ return { ...target, overview: true, resize: 'dynamic',
21
+ resizeRange: { minWidth: 320, minHeight: 320, maxWidth: 4096, maxHeight: 4096 },
22
+ modes: [{ width: 1280, height: 720 }, { width: 1920, height: 1080 }, { width: 1024, height: 768 }, { width: 720, height: 1280 }] };
23
+ if (actual.modes.length > 1)
24
+ return { ...target, overview: true, resize: 'modes', modes: actual.modes };
25
+ }
26
+ return { ...target, overview: true, resize: 'dynamic', captureViewport: true, capturePixelScale: 1,
27
+ resizeRange: { minWidth: 2, minHeight: 2, maxWidth: target.width & ~1, maxHeight: target.height & ~1 },
28
+ modes: captureSamplingModes(target.width, target.height) };
29
+ }
@@ -0,0 +1,222 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { z } from 'zod';
6
+ import { NativeProvider } from '../desktopControl/nativeProvider.js';
7
+ import { HelperProcess, NativeCommandError } from '../desktopControl/helperProcess.js';
8
+ import { desktopCapabilitiesSchema } from '../desktopControl/protocol.js';
9
+ import { captureTargetLabel } from '../desktopControl/captureTargetLabel.js';
10
+ import { normalizeWorkstationServer } from '../control/serverIdentity.js';
11
+ import { privateDirectory } from '../keepers/kit/paths.js';
12
+ import { verifyRdInstallation } from './artifact.js';
13
+ import { readRdState } from './state.js';
14
+ import { chooseSession, discoverSessions, ensureManagedSession, sessionAlive } from './sessions.js';
15
+ import { desktopDisplayControl, parseXrandr } from './display.js';
16
+ const execute = promisify(execFile);
17
+ const sourceAxis = z.number().int().min(2).max(4096);
18
+ const geometrySchema = z.object({ width: sourceAxis, height: sourceAxis, audio: z.boolean().optional() });
19
+ const windowSchema = z.array(z.object({ id: z.string().regex(/^[a-f0-9]{32}$/), kind: z.literal('window'), label: z.string().max(4096), width: sourceAxis, height: sourceAxis })).max(127);
20
+ export class LinuxDesktopProvider {
21
+ directory;
22
+ server;
23
+ legacy;
24
+ session;
25
+ provider;
26
+ state;
27
+ runtime = '';
28
+ revision = 0;
29
+ geometryIdentity = '';
30
+ initialization;
31
+ usingLegacy = false;
32
+ targetRevisions = new Map();
33
+ constructor(directory, server, legacy) {
34
+ this.directory = directory;
35
+ this.server = server;
36
+ this.legacy = legacy;
37
+ }
38
+ get automaticDpi() {
39
+ if (this.usingLegacy)
40
+ return true;
41
+ const setting = this.session?.environment.AXOM_DESKTOP_AUTO_DPI;
42
+ if (setting !== undefined)
43
+ return setting === '1';
44
+ return this.legacy !== undefined && this.session?.managed === true && this.session.environment.AXOM_DESKTOP_OWNED_DUMMY === '1';
45
+ }
46
+ async validate(session = this.session) {
47
+ const current = await readRdState(this.directory);
48
+ if (!current?.enabled || !this.state || current.artifact.sha512 !== this.state.artifact.sha512 ||
49
+ current.session !== this.state.session || current.display !== this.state.display ||
50
+ normalizeWorkstationServer(current.server) !== normalizeWorkstationServer(this.server) || !session || this.session !== session || !await sessionAlive(session)) {
51
+ throw new NativeCommandError('unavailable');
52
+ }
53
+ }
54
+ async initialize() {
55
+ if (this.initialization)
56
+ return this.initialization;
57
+ this.initialization = this.initializeCurrent();
58
+ try {
59
+ await this.initialization;
60
+ }
61
+ finally {
62
+ this.initialization = undefined;
63
+ }
64
+ }
65
+ async initializeCurrent() {
66
+ const state = await readRdState(this.directory);
67
+ this.usingLegacy = state === undefined && this.legacy !== undefined;
68
+ if (this.usingLegacy) {
69
+ this.provider = this.legacy;
70
+ this.state = undefined;
71
+ return;
72
+ }
73
+ if (!state?.enabled) {
74
+ this.provider = undefined;
75
+ this.state = undefined;
76
+ return;
77
+ }
78
+ if (normalizeWorkstationServer(state.server) !== normalizeWorkstationServer(this.server))
79
+ throw new Error('RD belongs to a different workstation server');
80
+ if (this.provider && this.state?.artifact.sha512 === state.artifact.sha512 && this.state.session === state.session && this.state.display === state.display && this.session && await sessionAlive(this.session))
81
+ return;
82
+ this.provider = undefined;
83
+ this.session = undefined;
84
+ await verifyRdInstallation(state.installation, state.artifact);
85
+ const session = chooseSession(await discoverSessions(), state) ?? await ensureManagedSession(this.directory);
86
+ this.session = session;
87
+ this.state = state;
88
+ const runtimeRoot = path.join(session.environment.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid?.()}`, 'axom-rd');
89
+ await privateDirectory(runtimeRoot);
90
+ this.runtime = session.managed && session.environment.AXOM_DESKTOP_RUNTIME_DIR && path.isAbsolute(session.environment.AXOM_DESKTOP_RUNTIME_DIR) ?
91
+ session.environment.AXOM_DESKTOP_RUNTIME_DIR : path.join(runtimeRoot, session.identity);
92
+ await privateDirectory(this.runtime);
93
+ const executable = path.join(state.installation, 'bin/axom-desktop-keeper');
94
+ this.provider = new NativeProvider({ helperPath: executable, runtimeDir: this.runtime,
95
+ environment: this.environment(), enumerate: () => this.capabilities(), validateSession: () => this.validate(session),
96
+ sessionHelperFactory: async (binding) => {
97
+ await this.validate(session);
98
+ const helper = await HelperProcess.launch(executable, { ...this.environment(), AXOM_KEEPER_TARGET: binding.displayId });
99
+ return helper;
100
+ } });
101
+ process.env.AXOM_QUIC_BINARY = path.join(state.installation, 'bin/axom-quic');
102
+ }
103
+ environment() {
104
+ return { ...process.env, ...this.session.environment, AXOM_DESKTOP_RUNTIME_DIR: this.runtime, AXOM_ACCESS_TOKEN: undefined,
105
+ ...(this.state.artifact.ipcVersion === 1 ? { XDG_RUNTIME_DIR: this.runtime } : {}),
106
+ AXOM_DESKTOP_DISPLAY_CONTROL: path.join(this.state.installation, 'bin/axom-display'),
107
+ AXOM_DESKTOP_PROVIDER_FILE: path.join(this.runtime, 'provider.json'),
108
+ AXOM_DESKTOP_OWNED_DUMMY: this.session.managed && this.state.artifact.ipcVersion === 2 ? '1' : undefined };
109
+ }
110
+ async capabilities() {
111
+ await this.validate();
112
+ const executable = path.join(this.state.installation, 'bin/axom-desktop-keeper');
113
+ const options = { env: this.environment(), timeout: 2000, maxBuffer: 49152 };
114
+ const { stdout } = await execute(executable, ['--probe'], options);
115
+ const probe = geometrySchema.parse(JSON.parse(stdout));
116
+ const geometry = { width: probe.width, height: probe.height };
117
+ const sharedRuntime = this.state.artifact.ipcVersion === 2;
118
+ let actual = { output: '', ...geometry, modes: [] };
119
+ if (sharedRuntime) {
120
+ try {
121
+ actual = parseXrandr((await execute('xrandr', ['--current'], options)).stdout);
122
+ }
123
+ catch (error) {
124
+ if (this.session.managed)
125
+ throw error;
126
+ }
127
+ }
128
+ let dpi;
129
+ if (sharedRuntime && this.session.managed) {
130
+ const resources = (await execute('xrdb', ['-query'], options)).stdout;
131
+ const value = Number(/^Xft\.dpi:\s*(\d+)\s*$/m.exec(resources)?.[1] ?? 96);
132
+ dpi = z.number().int().min(64).max(384).parse(value);
133
+ }
134
+ const geometryIdentity = `${this.session.identity}:${geometry.width}:${geometry.height}:${dpi ?? ''}`;
135
+ if (geometryIdentity !== this.geometryIdentity) {
136
+ this.revision++;
137
+ this.geometryIdentity = geometryIdentity;
138
+ }
139
+ const staging = path.join(this.runtime, '.geometry.json');
140
+ await fs.writeFile(staging, JSON.stringify({ ...geometry, ...(dpi === undefined ? {} : { dpi }) }), { mode: 0o600 });
141
+ await fs.rename(staging, path.join(this.runtime, 'geometry.json'));
142
+ const targets = windowSchema.parse(JSON.parse((await execute(executable, ['--capture-targets'], options)).stdout));
143
+ const root = { id: '00000000000000000000000000000001', kind: 'display', label: this.session.managed ? 'Virtual desktop' : 'Desktop', ...geometry };
144
+ const displays = [root, ...targets].map(target => {
145
+ const identity = `${this.session.identity}:${target.width}:${target.height}`;
146
+ const previous = this.targetRevisions.get(target.id);
147
+ const revision = target.id === root.id ? this.revision : previous ? previous.revision + Number(previous.geometry !== identity) : 1;
148
+ this.targetRevisions.set(target.id, { geometry: identity, revision });
149
+ const display = desktopDisplayControl({ ...target, label: captureTargetLabel(target.label), revision }, actual, sharedRuntime && this.session.managed);
150
+ return { ...display, ...(target.id === root.id && dpi !== undefined && display.captureViewport !== true ? {
151
+ dpi, maxDpi: 384, appearanceSync: true,
152
+ } : {}) };
153
+ });
154
+ const live = new Set(displays.map(display => display.id));
155
+ for (const id of this.targetRevisions.keys())
156
+ if (!live.has(id))
157
+ this.targetRevisions.delete(id);
158
+ const failed = await Promise.all(['display-failed', 'input-failed'].map(async (name) => {
159
+ try {
160
+ await fs.lstat(path.join(this.runtime, name));
161
+ return true;
162
+ }
163
+ catch (error) {
164
+ if (error.code !== 'ENOENT')
165
+ throw error;
166
+ return false;
167
+ }
168
+ }));
169
+ const capabilities = desktopCapabilitiesSchema.parse({ protocols: { min: 2, max: probe.audio ? 3 : 2 }, providerId: 'linux-x11-v1', providerVersion: 'axom-linux-2',
170
+ configured: true, permission: 'granted', health: failed.some(Boolean) ? 'unavailable' : 'ready', reason: failed.some(Boolean) ? 'recovery_unverified' : null,
171
+ cursorEmbedded: false, formats: ['h264-420-v1'], encoderBackends: [{ id: 'libx264', kind: 'software', formats: [{ format: 'h264-420-v1',
172
+ maxWidth: 4096, maxHeight: 4096, maxPixels: 4000000, maxFps: 30, cappedRate: true }] }],
173
+ input: { pointer: true, physicalKeyboard: true, unicodeText: true, clipboard: true, textPaste: true }, displays });
174
+ const manifest = path.join(this.runtime, '.provider.json');
175
+ await fs.writeFile(manifest, JSON.stringify(capabilities), { mode: 0o600 });
176
+ await fs.rename(manifest, path.join(this.runtime, 'provider.json'));
177
+ return capabilities;
178
+ }
179
+ async probe() {
180
+ try {
181
+ await this.initialize();
182
+ if (!this.provider)
183
+ return null;
184
+ if (this.usingLegacy)
185
+ return this.provider.probe();
186
+ const capabilities = await this.provider.probe();
187
+ if (!capabilities)
188
+ return null;
189
+ await fs.writeFile(path.join(this.directory, 'rd', 'status.json'), JSON.stringify({ updatedAt: new Date().toISOString(), state: capabilities.health,
190
+ session: this.session.managed ? 'virtual' : 'existing', display: this.session.display, width: capabilities.displays[0].width, height: capabilities.displays[0].height,
191
+ resolution: capabilities.displays[0].captureViewport === true ? 'sampling' : capabilities.displays[0].resize,
192
+ modes: capabilities.displays[0].captureViewport === true ? [] : capabilities.displays[0].modes }), { mode: 0o600 });
193
+ return capabilities;
194
+ }
195
+ catch (error) {
196
+ await fs.writeFile(path.join(this.directory, 'rd', 'status.json'), JSON.stringify({ updatedAt: new Date().toISOString(), state: 'unavailable', reason: String(error) }), { mode: 0o600 }).catch(() => undefined);
197
+ throw error;
198
+ }
199
+ }
200
+ async prepare(selection, binding) {
201
+ await this.initialize();
202
+ if (!this.provider)
203
+ throw new NativeCommandError('unavailable');
204
+ return this.provider.prepare(selection, binding);
205
+ }
206
+ async wallpaper(targetId) {
207
+ await this.initialize();
208
+ if (!this.usingLegacy)
209
+ await this.validate();
210
+ if (!this.provider)
211
+ throw new NativeCommandError('unavailable');
212
+ return this.provider.wallpaper(targetId);
213
+ }
214
+ async recoverDisplay(binding, reservation) {
215
+ await this.initialize();
216
+ if (!this.usingLegacy)
217
+ await this.validate();
218
+ if (!this.provider)
219
+ throw new NativeCommandError('unavailable');
220
+ return this.provider.recoverDisplay(binding, reservation);
221
+ }
222
+ }