@platless/axom-cli 0.5.9 → 0.6.52
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.
- package/README.md +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -35,6 +35,7 @@ Type=simple
|
|
|
35
35
|
WorkingDirectory=${systemdToken(context.homeDir)}
|
|
36
36
|
${environment('HOME', context.homeDir)}
|
|
37
37
|
${environment('AXOM_WORKSTATION_CONFIG_DIR', context.configDir)}
|
|
38
|
+
${environment('AXOM_WORKSTATION_RUNTIME', 'device-v2')}
|
|
38
39
|
${environment('AXOM_WORKSTATION_NPM_PATH', context.npmPath)}
|
|
39
40
|
${environment('AXOM_WORKSTATION_NPM_PREFIX', context.npmPrefix)}
|
|
40
41
|
${environment('PATH', context.pathEnv)}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const LOCAL_FILESYSTEMS = new Set([
|
|
7
|
+
'ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'f2fs', 'bcachefs',
|
|
8
|
+
'vfat', 'exfat', 'ntfs', 'ntfs3', 'fuseblk',
|
|
9
|
+
]);
|
|
10
|
+
export function linuxStoragePools(mounts) {
|
|
11
|
+
const pools = new Map();
|
|
12
|
+
for (const mount of mounts) {
|
|
13
|
+
if (!LOCAL_FILESYSTEMS.has(mount.fstype) || mount.options?.split(',').includes('ro'))
|
|
14
|
+
continue;
|
|
15
|
+
if (/^\/(?:boot|efi)(?:\/|$)/.test(mount.target))
|
|
16
|
+
continue;
|
|
17
|
+
const source = mount.source.split('[')[0];
|
|
18
|
+
const key = mount.fstype === 'zfs'
|
|
19
|
+
? `zfs:${source.split('/')[0]}`
|
|
20
|
+
: mount.uuid ? `${mount.fstype}:${mount.uuid}` : `dev:${mount['maj:min']}`;
|
|
21
|
+
const existing = pools.get(key);
|
|
22
|
+
if (existing)
|
|
23
|
+
existing.paths.push(mount.target);
|
|
24
|
+
else
|
|
25
|
+
pools.set(key, { key, label: mount.label || source, paths: [mount.target] });
|
|
26
|
+
}
|
|
27
|
+
if (pools.size === 0) {
|
|
28
|
+
const root = mounts.find((mount) => mount.target === '/' && mount.fstype === 'overlay');
|
|
29
|
+
if (root)
|
|
30
|
+
pools.set('overlay:/', { key: 'overlay:/', label: 'Container storage', paths: ['/'] });
|
|
31
|
+
}
|
|
32
|
+
return [...pools.values()];
|
|
33
|
+
}
|
|
34
|
+
export function darwinStoragePools(disks, physicalDisks, containers) {
|
|
35
|
+
const physical = new Set(physicalDisks.flatMap((disk) => [disk.DeviceIdentifier, ...(disk.Partitions ?? []).map((part) => part.DeviceIdentifier)]));
|
|
36
|
+
const volumes = disks.flatMap((disk) => [disk, ...(disk.Partitions ?? []), ...(disk.APFSVolumes ?? [])]);
|
|
37
|
+
const pools = [];
|
|
38
|
+
for (const container of containers) {
|
|
39
|
+
if (!container.PhysicalStores.some((store) => physical.has(store.DeviceIdentifier)))
|
|
40
|
+
continue;
|
|
41
|
+
const userVolumes = container.Volumes.filter((volume) => volume.Roles.length === 0 || volume.Roles.includes('Data'));
|
|
42
|
+
userVolumes.sort((first, second) => Number(second.Roles.includes('Data')) - Number(first.Roles.includes('Data')));
|
|
43
|
+
const mounted = userVolumes.flatMap((volume) => {
|
|
44
|
+
const disk = volumes.find((candidate) => candidate.DeviceIdentifier === volume.DeviceIdentifier);
|
|
45
|
+
return disk?.MountPoint ? [{ name: volume.Name, mount: disk.MountPoint }] : [];
|
|
46
|
+
});
|
|
47
|
+
if (mounted.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
const system = container.Volumes.find((volume) => volume.Roles.includes('System'));
|
|
50
|
+
pools.push({
|
|
51
|
+
key: `apfs:${container.APFSContainerUUID}`,
|
|
52
|
+
label: system?.Name || mounted[0].name,
|
|
53
|
+
paths: mounted.map((volume) => volume.mount),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
for (const volume of volumes) {
|
|
57
|
+
if (!physical.has(volume.DeviceIdentifier) || !volume.MountPoint)
|
|
58
|
+
continue;
|
|
59
|
+
if (!['Apple_HFS', 'Microsoft Basic Data', 'Windows_NTFS', 'DOS_FAT_32', 'DOS_FAT_16', 'ExFAT'].includes(volume.Content ?? ''))
|
|
60
|
+
continue;
|
|
61
|
+
pools.push({
|
|
62
|
+
key: `volume:${volume.VolumeUUID || volume.DiskUUID || volume.DeviceIdentifier}`,
|
|
63
|
+
label: volume.VolumeName || volume.MountPoint,
|
|
64
|
+
paths: [volume.MountPoint],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return pools;
|
|
68
|
+
}
|
|
69
|
+
async function diskutilJson(args) {
|
|
70
|
+
const { stdout } = await execFileAsync('/usr/sbin/diskutil', args, {
|
|
71
|
+
timeout: 5_000, maxBuffer: 4 * 1024 * 1024,
|
|
72
|
+
});
|
|
73
|
+
const json = await new Promise((resolve, reject) => {
|
|
74
|
+
const child = execFile('/usr/bin/plutil', ['-convert', 'json', '-o', '-', '-'], {
|
|
75
|
+
timeout: 2_000, maxBuffer: 4 * 1024 * 1024,
|
|
76
|
+
}, (error, output) => error ? reject(error) : resolve(output));
|
|
77
|
+
child.stdin?.on('error', reject);
|
|
78
|
+
child.stdin?.end(stdout);
|
|
79
|
+
});
|
|
80
|
+
return JSON.parse(json);
|
|
81
|
+
}
|
|
82
|
+
export async function discoverStoragePools(platform, workspaces) {
|
|
83
|
+
if (platform === 'linux') {
|
|
84
|
+
const { stdout } = await execFileAsync('findmnt', [
|
|
85
|
+
'--json', '--list', '--output', 'TARGET,SOURCE,FSTYPE,UUID,LABEL,MAJ:MIN,OPTIONS',
|
|
86
|
+
], { timeout: 5_000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } });
|
|
87
|
+
const mounts = JSON.parse(stdout).filesystems;
|
|
88
|
+
return linuxStoragePools(mounts.filter((mount) => {
|
|
89
|
+
try {
|
|
90
|
+
return statSync(mount.target).isDirectory();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
if (platform === 'darwin') {
|
|
98
|
+
const [all, physical, apfs] = await Promise.all([
|
|
99
|
+
diskutilJson(['list', '-plist']),
|
|
100
|
+
diskutilJson(['list', '-plist', 'physical']),
|
|
101
|
+
diskutilJson(['apfs', 'list', '-plist']),
|
|
102
|
+
]);
|
|
103
|
+
return darwinStoragePools(all.AllDisksAndPartitions, physical.AllDisksAndPartitions, apfs.Containers ?? []);
|
|
104
|
+
}
|
|
105
|
+
const pools = new Map();
|
|
106
|
+
for (const { cwd } of workspaces) {
|
|
107
|
+
try {
|
|
108
|
+
const key = `dev:${statSync(cwd).dev}`;
|
|
109
|
+
const existing = pools.get(key);
|
|
110
|
+
if (existing)
|
|
111
|
+
existing.paths.push(cwd);
|
|
112
|
+
else
|
|
113
|
+
pools.set(key, { key, label: path.parse(cwd).root, paths: [cwd] });
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}
|
|
117
|
+
return [...pools.values()];
|
|
118
|
+
}
|
|
@@ -2,8 +2,8 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readFileSync, statfsSync, statSync } from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { discoverStoragePools } from './storageMetrics.js';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
export const STORAGE_TOPOLOGY_REFRESH_MS = 10 * 60 * 1000;
|
|
9
9
|
function cpuTotals(cpus) {
|
|
@@ -36,45 +36,6 @@ function linuxMemoryPressure(readFile) {
|
|
|
36
36
|
function stablePoolId(key) {
|
|
37
37
|
return createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
38
38
|
}
|
|
39
|
-
async function defaultIdentifyStorage(platform, cwd, device) {
|
|
40
|
-
try {
|
|
41
|
-
if (platform === 'darwin') {
|
|
42
|
-
const mounted = await execFileAsync('/bin/df', ['-P', cwd], {
|
|
43
|
-
timeout: 1_000,
|
|
44
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
45
|
-
});
|
|
46
|
-
const mountFields = mounted.stdout.trim().split('\n').at(-1)?.trim().split(/\s+/) ?? [];
|
|
47
|
-
const mountPoint = mountFields.slice(5).join(' ') || path.parse(cwd).root;
|
|
48
|
-
const { stdout } = await execFileAsync('/usr/sbin/diskutil', ['info', mountPoint], {
|
|
49
|
-
timeout: 1_000,
|
|
50
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
51
|
-
});
|
|
52
|
-
const container = /^\s*APFS Container Reference:\s*(\S+)\s*$/m.exec(stdout)?.[1];
|
|
53
|
-
const volume = /^\s*Volume Name:\s*(.+?)\s*$/m.exec(stdout)?.[1];
|
|
54
|
-
return container
|
|
55
|
-
? { key: `apfs:${container}`, label: `APFS Container ${container}` }
|
|
56
|
-
: { key: `dev:${device}`, label: volume };
|
|
57
|
-
}
|
|
58
|
-
if (platform === 'linux') {
|
|
59
|
-
const { stdout } = await execFileAsync('findmnt', ['-n', '-P', '-o', 'FSTYPE,SOURCE,UUID,LABEL,TARGET', '--target', cwd], {
|
|
60
|
-
timeout: 2_000,
|
|
61
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
62
|
-
});
|
|
63
|
-
const fields = Object.fromEntries([...stdout.matchAll(/(\w+)="([^"]*)"/g)].map((match) => [match[1], match[2]]));
|
|
64
|
-
if (fields.FSTYPE === 'btrfs' && fields.UUID)
|
|
65
|
-
return { key: `btrfs:${fields.UUID}`, label: fields.SOURCE };
|
|
66
|
-
if (fields.FSTYPE === 'zfs' && fields.SOURCE) {
|
|
67
|
-
const pool = fields.SOURCE.split('/')[0];
|
|
68
|
-
return { key: `zfs:${pool}`, label: pool };
|
|
69
|
-
}
|
|
70
|
-
return { key: `dev:${device}`, label: fields.LABEL || fields.TARGET || fields.SOURCE };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Resource-pool recognition is best-effort. Device identity below is the conservative fallback.
|
|
75
|
-
}
|
|
76
|
-
return { key: `dev:${device}` };
|
|
77
|
-
}
|
|
78
39
|
async function defaultDarwinAvailableMemory() {
|
|
79
40
|
try {
|
|
80
41
|
const { stdout } = await execFileAsync('/usr/bin/vm_stat', [], { timeout: 2_000 });
|
|
@@ -216,7 +177,7 @@ export class SystemMetricsSampler {
|
|
|
216
177
|
readFile: readFileSync,
|
|
217
178
|
statfs: statfsSync,
|
|
218
179
|
stat: statSync,
|
|
219
|
-
|
|
180
|
+
discoverStorage: () => discoverStoragePools(process.platform, workspaces),
|
|
220
181
|
darwinAvailableMemory: defaultDarwinAvailableMemory,
|
|
221
182
|
darwinMemoryPressure: defaultDarwinMemoryPressure,
|
|
222
183
|
probeGpus: () => defaultProbeGpus(process.platform),
|
|
@@ -234,6 +195,9 @@ export class SystemMetricsSampler {
|
|
|
234
195
|
clearInterval(this.refreshTimer);
|
|
235
196
|
this.refreshTimer = null;
|
|
236
197
|
}
|
|
198
|
+
refreshWorkspaces() {
|
|
199
|
+
this.refreshTopologyInBackground();
|
|
200
|
+
}
|
|
237
201
|
snapshot() {
|
|
238
202
|
try {
|
|
239
203
|
this.refreshDarwinMemoryInBackground();
|
|
@@ -261,20 +225,23 @@ export class SystemMetricsSampler {
|
|
|
261
225
|
: (this.deps.platform === 'darwin' ? this.darwinMemoryPressure : null);
|
|
262
226
|
const pools = new Map();
|
|
263
227
|
for (const pool of this.storage) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
228
|
+
for (const cwd of pool.paths) {
|
|
229
|
+
try {
|
|
230
|
+
if (String(this.deps.stat(cwd).dev) !== pool.devices[cwd])
|
|
231
|
+
continue;
|
|
232
|
+
const stats = this.deps.statfs(cwd);
|
|
233
|
+
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
234
|
+
const availableBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
235
|
+
pools.set(pool.key, {
|
|
236
|
+
id: stablePoolId(pool.key),
|
|
237
|
+
label: pool.label,
|
|
238
|
+
workspaces: pool.workspaces,
|
|
239
|
+
totalBytes,
|
|
240
|
+
availableBytes,
|
|
241
|
+
});
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
278
245
|
}
|
|
279
246
|
}
|
|
280
247
|
return {
|
|
@@ -338,43 +305,31 @@ export class SystemMetricsSampler {
|
|
|
338
305
|
this.darwinMemoryPressure = pressure;
|
|
339
306
|
}
|
|
340
307
|
async refreshTopology() {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
catch {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
const grouped = new Map();
|
|
371
|
-
for (const entry of identified.filter((candidate) => candidate !== null)) {
|
|
372
|
-
const existing = grouped.get(entry.key);
|
|
373
|
-
if (existing)
|
|
374
|
-
existing.workspaces.push(entry.name);
|
|
375
|
-
else
|
|
376
|
-
grouped.set(entry.key, { cwd: entry.cwd, key: entry.key, label: entry.label, workspaces: [entry.name] });
|
|
308
|
+
try {
|
|
309
|
+
const pools = await this.deps.discoverStorage();
|
|
310
|
+
this.storage = pools.map((pool) => {
|
|
311
|
+
const devices = Object.fromEntries(pool.paths.flatMap((cwd) => {
|
|
312
|
+
try {
|
|
313
|
+
return [[cwd, String(this.deps.stat(cwd).dev)]];
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}));
|
|
319
|
+
const poolDevices = new Set(Object.values(devices));
|
|
320
|
+
const workspaces = this.workspaces.filter(({ cwd }) => {
|
|
321
|
+
try {
|
|
322
|
+
return poolDevices.has(String(this.deps.stat(cwd).dev));
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}).map(({ name }) => name);
|
|
328
|
+
return { ...pool, workspaces, devices };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return;
|
|
377
333
|
}
|
|
378
|
-
this.storage = [...grouped.values()];
|
|
379
334
|
}
|
|
380
335
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
4
|
+
const VERIFY_TIMEOUT_MS = 3_000;
|
|
5
|
+
/** Interpreters rarely appear or vanish; a slow refresh keeps the heartbeat itself free of any spawning. */
|
|
6
|
+
export const TOOLCHAIN_REFRESH_MS = 15 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* Shells whose `-lc` runs a POSIX-ish command string with `command -v` (fish ≥ 3 included). Any other shell (csh, tcsh,
|
|
9
|
+
* nu, pwsh, …) is not probed: its answer is unknown, never "missing".
|
|
10
|
+
*/
|
|
11
|
+
const PROBED_SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'mksh', 'fish']);
|
|
12
|
+
/** Separates the probe's answers; no leading `=` (zsh would expand a bare `=word`). */
|
|
13
|
+
const SPLIT = 'AXOM-PROBE-SPLIT';
|
|
14
|
+
const DONE = 'AXOM-PROBE-DONE';
|
|
15
|
+
const NAMES = ['python3', 'python', 'node'];
|
|
16
|
+
/**
|
|
17
|
+
* One `<shell> -lc` run of `command -v` for each interpreter, the way commands run on this workstation (exec spawns the
|
|
18
|
+
* same login shell), so a Python found only through the user's profile counts. Each answer is the last absolute-path
|
|
19
|
+
* line of its segment (a login profile may print its own lines first). Null = unknown: a shell we do not probe, a shell
|
|
20
|
+
* that could not run, or a run that did not reach its end marker.
|
|
21
|
+
*/
|
|
22
|
+
export async function probeToolchain(shell, env = process.env, options = {}) {
|
|
23
|
+
if (!PROBED_SHELLS.has(path.basename(shell)))
|
|
24
|
+
return null;
|
|
25
|
+
const script = [...NAMES.map((name) => `command -v ${name}`), `echo ${DONE}`].join(`; echo ${SPLIT}; `);
|
|
26
|
+
const stdout = await run(shell, ['-lc', script], env, PROBE_TIMEOUT_MS);
|
|
27
|
+
if (stdout === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
const lines = stdout.split('\n').map((line) => line.trim());
|
|
30
|
+
const doneAt = lines.lastIndexOf(DONE);
|
|
31
|
+
if (doneAt < 0)
|
|
32
|
+
return null;
|
|
33
|
+
const segments = [[]];
|
|
34
|
+
for (const line of lines.slice(0, doneAt)) {
|
|
35
|
+
if (line === SPLIT)
|
|
36
|
+
segments.push([]);
|
|
37
|
+
else
|
|
38
|
+
segments[segments.length - 1].push(line);
|
|
39
|
+
}
|
|
40
|
+
// `command -v python3; echo SPLIT; …; echo SPLIT; echo DONE`: one segment per name, then an empty one. A login
|
|
41
|
+
// profile's own output lands in the first segment, before the answer, so each answer is its segment's last path.
|
|
42
|
+
if (segments.length !== NAMES.length + 1)
|
|
43
|
+
return null;
|
|
44
|
+
const answers = segments.slice(0, NAMES.length);
|
|
45
|
+
const found = (index) => [...answers[index]].reverse().find((line) => path.isAbsolute(line)) ?? null;
|
|
46
|
+
let python3 = found(0);
|
|
47
|
+
if (python3 === '/usr/bin/python3' && (options.platform ?? process.platform) === 'darwin') {
|
|
48
|
+
if (!(await (options.macStubRuns ?? macCommandLineToolsInstalled)()))
|
|
49
|
+
python3 = null;
|
|
50
|
+
}
|
|
51
|
+
return { python: python3 ?? found(1), node: found(2) };
|
|
52
|
+
}
|
|
53
|
+
async function macCommandLineToolsInstalled() {
|
|
54
|
+
return (await run('/usr/bin/xcode-select', ['-p'], process.env, VERIFY_TIMEOUT_MS, true)) !== undefined;
|
|
55
|
+
}
|
|
56
|
+
/** The command's stdout; undefined = it did not run, timed out, or (with [strict]) exited non-zero. */
|
|
57
|
+
function run(file, args, env, timeout, strict = false) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
execFile(file, args, { env, timeout, windowsHide: true }, (error, stdout) => {
|
|
60
|
+
const code = error?.code;
|
|
61
|
+
if (error && (strict || typeof code !== 'number')) {
|
|
62
|
+
resolve(undefined);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(String(stdout));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Holds the latest probe; the first answer arrives shortly after start, until then the heartbeat simply omits it. */
|
|
70
|
+
export class ToolchainProbe {
|
|
71
|
+
shell;
|
|
72
|
+
env;
|
|
73
|
+
latest = null;
|
|
74
|
+
timer;
|
|
75
|
+
constructor(shell, env = () => process.env) {
|
|
76
|
+
this.shell = shell;
|
|
77
|
+
this.env = env;
|
|
78
|
+
}
|
|
79
|
+
current() { return this.latest; }
|
|
80
|
+
start() {
|
|
81
|
+
if (process.platform === 'win32' || this.timer)
|
|
82
|
+
return;
|
|
83
|
+
const refresh = async () => {
|
|
84
|
+
const found = await probeToolchain(this.shell(), this.env()).catch(() => null);
|
|
85
|
+
if (found)
|
|
86
|
+
this.latest = found;
|
|
87
|
+
};
|
|
88
|
+
void refresh();
|
|
89
|
+
this.timer = setInterval(() => { void refresh(); }, TOOLCHAIN_REFRESH_MS);
|
|
90
|
+
this.timer.unref();
|
|
91
|
+
}
|
|
92
|
+
stop() { if (this.timer)
|
|
93
|
+
clearInterval(this.timer); this.timer = undefined; }
|
|
94
|
+
}
|
|
@@ -17,13 +17,31 @@ async function bounded(promise, timeout = TIMEOUT) {
|
|
|
17
17
|
clearTimeout(timer);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* All ancestors are checked: mkdir recursive must never follow a planted profile symlink. The one
|
|
22
|
+
* symlinks allowed across are root-owned system symlinks (`/var -> /private/var` on macOS): stock
|
|
23
|
+
* macOS tmp dirs sit behind one, and refusing it would reject every per-user tmpdir. A root-owned
|
|
24
|
+
* symlink whose target is a root-owned, non-group-writable directory is not plantable by the user
|
|
25
|
+
* this runs as; anything else — every user-owned symlink — still fails the check below.
|
|
26
|
+
*/
|
|
21
27
|
async function ownedDirectory(dir, privateMode = false) {
|
|
22
28
|
const absolute = path.resolve(dir);
|
|
23
29
|
const parts = absolute.split(path.sep).filter(Boolean);
|
|
24
30
|
let current = path.parse(absolute).root;
|
|
25
31
|
const uid = process.getuid?.();
|
|
26
32
|
let protectedByOwner = false;
|
|
33
|
+
const systemSymlink = async (stat) => {
|
|
34
|
+
if (stat.uid !== 0 || uid === 0)
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
const target = await fs.realpath(current);
|
|
38
|
+
const targetStat = await fs.lstat(target);
|
|
39
|
+
return targetStat.isDirectory() && targetStat.uid === 0 && (targetStat.mode & 0o022) === 0;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
27
45
|
for (const part of parts) {
|
|
28
46
|
current = path.join(current, part);
|
|
29
47
|
await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
|
|
@@ -31,8 +49,14 @@ async function ownedDirectory(dir, privateMode = false) {
|
|
|
31
49
|
throw e;
|
|
32
50
|
});
|
|
33
51
|
const stat = await fs.lstat(current);
|
|
52
|
+
if (stat.isSymbolicLink()) {
|
|
53
|
+
if (!(await systemSymlink(stat)))
|
|
54
|
+
throw Error("Unsafe browser directory");
|
|
55
|
+
// Root-owned system symlink: cross it; the link itself carries no mode or ownership
|
|
56
|
+
// expectation (Linux reports links 0777), its target was just verified.
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
34
59
|
if (!stat.isDirectory() ||
|
|
35
|
-
stat.isSymbolicLink() ||
|
|
36
60
|
(!protectedByOwner &&
|
|
37
61
|
(stat.mode & 0o022) !== 0 &&
|
|
38
62
|
!(stat.mode & 0o1000)) ||
|
|
@@ -44,11 +68,11 @@ async function ownedDirectory(dir, privateMode = false) {
|
|
|
44
68
|
if (uid !== undefined && stat.uid === uid && (stat.mode & 0o077) === 0)
|
|
45
69
|
protectedByOwner = true;
|
|
46
70
|
}
|
|
47
|
-
const stat = await fs.lstat(
|
|
71
|
+
const stat = await fs.lstat(current);
|
|
48
72
|
if (process.getuid && stat.uid !== process.getuid())
|
|
49
73
|
throw Error("Browser directory is not owned by this user");
|
|
50
74
|
if (privateMode)
|
|
51
|
-
await fs.chmod(
|
|
75
|
+
await fs.chmod(current, 0o700);
|
|
52
76
|
}
|
|
53
77
|
function allowedUrl(params) {
|
|
54
78
|
const url = string(params, "url", 8192, "about:blank");
|
|
@@ -8,16 +8,13 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES, validateRequest } from './validation.js';
|
|
10
10
|
/** Unix socket paths must remain below macOS/Linux's ~104-byte sockaddr_un limit. */
|
|
11
|
-
export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = '
|
|
12
|
-
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server,
|
|
13
|
-
const namespace = `axom
|
|
14
|
-
// macOS /var/folders/.../T often exhausts sockaddr_un before adding a private namespace. Honor
|
|
15
|
-
// TMPDIR where it fits on Linux; the short system temporary root is checked for private ownership.
|
|
11
|
+
export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = 'browser') {
|
|
12
|
+
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, 'browser'])).digest('hex').slice(0, 32);
|
|
13
|
+
const namespace = `axom-browser-${process.getuid?.() ?? 'user'}`;
|
|
16
14
|
let dir = path.join(platform === 'darwin' ? '/tmp' : temporaryDirectory, namespace, identity);
|
|
17
15
|
if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
|
|
18
16
|
dir = path.join('/tmp', namespace, identity);
|
|
19
|
-
|
|
20
|
-
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: kind === 'browser' ? 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000 : 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-${kind === 'browser' ? 'browser' : 'tools'}-keeper-${identity}` };
|
|
17
|
+
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000, unit: `axom-browser-keeper-${identity}` };
|
|
21
18
|
}
|
|
22
19
|
export async function privateDirectory(dir) {
|
|
23
20
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
@@ -84,22 +81,16 @@ function pidAlive(pid) {
|
|
|
84
81
|
return error.code === 'EPERM';
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
|
-
/**
|
|
88
|
-
* A tools keeper client. The daemon only ever makes the browser kind (`browser`). The kind is required — no default — so nothing can
|
|
89
|
-
* start the retired terminal-kind tools keeper by accident (spec §9; terminals run in keeper-terminal, files in the daemon's files
|
|
90
|
-
* host); an explicit `terminal` client exists only for the tests that pin how the retired keeper's startup and tools.info behave.
|
|
91
|
-
* (`keeperPaths` keeps its terminal default: `keepers/legacy.ts` finds a legacy tools keeper an older CLI left running by it.)
|
|
92
|
-
*/
|
|
84
|
+
/** Browser supervisor client. Native terminals use TerminalKeeperClient exclusively. */
|
|
93
85
|
export class ToolsClient {
|
|
94
86
|
options;
|
|
95
87
|
kind;
|
|
96
88
|
paths;
|
|
97
89
|
starting;
|
|
98
|
-
browser;
|
|
99
90
|
constructor(options, kind) {
|
|
100
91
|
this.options = options;
|
|
101
92
|
this.kind = kind;
|
|
102
|
-
if (kind !== '
|
|
93
|
+
if (kind !== 'browser')
|
|
103
94
|
throw new Error('ToolsClient needs an explicit kind (browser)');
|
|
104
95
|
this.paths = keeperPaths(options, undefined, undefined, kind);
|
|
105
96
|
}
|
|
@@ -121,26 +112,16 @@ export class ToolsClient {
|
|
|
121
112
|
// Desktop sharing is optional. Checking terminal/files must not start a
|
|
122
113
|
// separate browser or require its dependencies when no VNC was configured.
|
|
123
114
|
const metadata = { browser: false, browserMode: 'shared', browserBinding: browserCapability(undefined) };
|
|
124
|
-
return
|
|
115
|
+
return metadata;
|
|
125
116
|
}
|
|
126
117
|
if (binding && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
127
118
|
const metadata = { browser: true, browserMode: 'shared', browserBinding: browserCapability(binding) };
|
|
128
119
|
if (method === 'browser.list')
|
|
129
120
|
return { ...metadata, sessions: [] };
|
|
130
121
|
if (method === 'tools.info')
|
|
131
|
-
return
|
|
122
|
+
return metadata;
|
|
132
123
|
throw new Error('Shared browser uses the live desktop viewer; legacy browser session operations are unavailable');
|
|
133
124
|
}
|
|
134
|
-
if (this.kind === 'terminal' && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
135
|
-
this.browser ??= new ToolsClient(this.options, 'browser');
|
|
136
|
-
if (method.startsWith('browser.'))
|
|
137
|
-
return this.browser.request(method, params);
|
|
138
|
-
const [terminal, browser] = await Promise.all([this.localRequest(method, params), this.browser.request(method, params).catch(error => ({ browser: false, browserError: error.message }))]);
|
|
139
|
-
const result = { ...terminal, ...browser, browserMode: 'legacy', browserBinding: browserCapability(undefined) };
|
|
140
|
-
if (result.browser)
|
|
141
|
-
delete result.browserError;
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
125
|
return this.localRequest(method, params);
|
|
145
126
|
}
|
|
146
127
|
async localRequest(method, params) {
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the legacy tools keeper gave it (tools/core.ts at the merge-base, since deleted, + tools/client.ts), and answering it never
|
|
9
|
-
* starts any keeper:
|
|
10
|
-
* - file fields come from the files host's last `ready` (or are computed here before the first one); `files: true`
|
|
11
|
-
* is reported even while the host restarts, so a probe never mistakes the machine for an old CLI;
|
|
12
|
-
* - terminal fields come from keeper-terminal's hello capabilities when that keeper is already running (connecting to it,
|
|
13
|
-
* never starting it: keepers/terminal/client.ts `terminalStatus`), otherwise from a cached node-pty load check
|
|
14
|
-
* (update/ptyPreflight.ts);
|
|
15
|
-
* - browser fields come from the browser-kind ToolsClient, whose `tools.info` only returns metadata.
|
|
16
|
-
*/
|
|
4
|
+
import { TERMINAL_GRID_SCHEMA, TERMINAL_GRID_CAPABILITY } from '../keepers/terminal/gridProtocol.js';
|
|
5
|
+
/* tools.info is read-only discovery. Schema/capability describe the native contract; terminal availability
|
|
6
|
+
* comes from a current running keeper hello. The daemon's absent-keeper source is false, never a load probe
|
|
7
|
+
* promising that a future keeper will start. Files/browser discovery retain their independent metadata. */
|
|
17
8
|
/** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
|
|
18
9
|
export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
|
|
19
10
|
export async function aggregateToolsInfo(sources) {
|
|
@@ -21,6 +12,8 @@ export async function aggregateToolsInfo(sources) {
|
|
|
21
12
|
return {
|
|
22
13
|
version: 1,
|
|
23
14
|
terminal: terminal.terminal,
|
|
15
|
+
terminalSchema: TERMINAL_GRID_SCHEMA,
|
|
16
|
+
terminalCapability: TERMINAL_GRID_CAPABILITY,
|
|
24
17
|
browser: false,
|
|
25
18
|
...files,
|
|
26
19
|
...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),
|