@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.
- package/README.md +18 -5
- package/dist/axom-patch/cli/README.md +100 -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/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/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -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 +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- 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/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -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/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- 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 +28 -0
- 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 +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- 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/provider.js} +0 -0
|
@@ -1,46 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { clientDir
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { clientDir } from './config.js';
|
|
3
3
|
import { osInfo } from './osinfo.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
os: osInfo(),
|
|
11
|
-
defaultWorkspace: { name: ws.name, cwd: ws.cwd, shell: ws.shell },
|
|
12
|
-
};
|
|
4
|
+
import { workstationEndpoint } from './control/serverIdentity.js';
|
|
5
|
+
import { LocalControlError } from './control/protocol.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const registerResponseSchema = z.object({ workstationId: z.string().min(1), ownerUserId: z.string().min(1).optional(), clientKey: z.string(), serverPub: z.string() });
|
|
8
|
+
/** No persistence; callers own the operation lock and identity transaction. */
|
|
9
|
+
export async function requestWorkstationRegistration(server, clientName, token, ws, workstationId, shell = ws?.shell ?? '/bin/sh') {
|
|
13
10
|
let res;
|
|
14
11
|
try {
|
|
15
|
-
res = await fetch(
|
|
16
|
-
method: 'POST',
|
|
12
|
+
res = await fetch(workstationEndpoint(server, '/workstation/register'), { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30_000),
|
|
17
13
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
18
|
-
body: JSON.stringify(
|
|
14
|
+
body: JSON.stringify({ name: clientName, ...(workstationId ? { workstationId } : {}), os: osInfo(), shell, ...(ws ? { defaultWorkspace: ws } : {}) }),
|
|
19
15
|
});
|
|
20
16
|
}
|
|
21
|
-
catch
|
|
22
|
-
throw new
|
|
17
|
+
catch {
|
|
18
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration was sent but its outcome could not be confirmed. Check your workstation list before trying again.');
|
|
19
|
+
}
|
|
20
|
+
if (res.status === 409)
|
|
21
|
+
throw new LocalControlError('BINDING_CONFLICT', 'A workstation with this name is already registered. Check Settings → Workstations.');
|
|
22
|
+
if (res.status === 401 || res.status === 403)
|
|
23
|
+
throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation permission is required. Run `axom auth` and select workstation, then try again.');
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration could not be confirmed. Check your workstation list.');
|
|
26
|
+
try {
|
|
27
|
+
return registerResponseSchema.parse(await res.json());
|
|
23
28
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// this same machine registered before an axom-cli upgrade and no longer has a local config at
|
|
27
|
-
// the new config-directory path (see the client's move from ~/.axom-workstation to
|
|
28
|
-
// ~/.axom/workstation). Rebinding the existing row silently would hand this machine's identity
|
|
29
|
-
// to whichever caller re-registers next, so the server refuses instead of upserting.
|
|
30
|
-
if (res.status === 409) {
|
|
31
|
-
throw new UsageError(`A workstation named '${clientName}' is already registered for this account. This is expected `
|
|
32
|
-
+ `when the same machine re-registers after an axom-cli upgrade. Delete it under Settings → `
|
|
33
|
-
+ 'Workstations, then run `axom workstation start` again.');
|
|
34
|
-
}
|
|
35
|
-
if (res.status === 403) {
|
|
36
|
-
throw new UsageError('This credential is not authorized to register a workstation. '
|
|
37
|
-
+ 'Run `axom auth` and select workstation, then try again.');
|
|
38
|
-
}
|
|
39
|
-
throw new Error(`Registration failed: ${res.status} ${await res.text()}`);
|
|
29
|
+
catch {
|
|
30
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration response was incomplete. Check your workstation list.');
|
|
40
31
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await
|
|
45
|
-
await writeKeys(dir, data.clientKey, data.serverPub);
|
|
32
|
+
}
|
|
33
|
+
export async function registerWorkstation(server, clientName, token, ws) {
|
|
34
|
+
const { enrollLegacyWorkstation } = await import('./control/enrollment.js');
|
|
35
|
+
await enrollLegacyWorkstation({ server, name: clientName, accessToken: token, workspace: ws, operationId: randomUUID() }, clientDir());
|
|
46
36
|
}
|
|
@@ -58,6 +58,22 @@ async function assertWritable(directory) {
|
|
|
58
58
|
}
|
|
59
59
|
const defaultDeps = {
|
|
60
60
|
run: runProcess,
|
|
61
|
+
readLaunchAgent: async (file) => {
|
|
62
|
+
const handle = await fs.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
63
|
+
try {
|
|
64
|
+
const stat = await handle.stat();
|
|
65
|
+
if (!stat.isFile() || stat.size > 65536)
|
|
66
|
+
throw new Error('Invalid LaunchAgent');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await handle.close();
|
|
70
|
+
}
|
|
71
|
+
const result = await runProcess('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file]);
|
|
72
|
+
const value = JSON.parse(result.stdout);
|
|
73
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
74
|
+
throw new Error('Invalid LaunchAgent');
|
|
75
|
+
return value;
|
|
76
|
+
},
|
|
61
77
|
writeTemp,
|
|
62
78
|
assertWritable,
|
|
63
79
|
ensureDirectory: async (directory) => { await fs.mkdir(directory, { recursive: true }); },
|
|
@@ -219,24 +235,38 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
219
235
|
// durable LaunchAgent plist. This keeps explicit stop meaningful across both service managers.
|
|
220
236
|
if (action === 'stop')
|
|
221
237
|
return deps.run('launchctl', ['bootout', launchdDomain]);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
return launchdStart();
|
|
239
|
+
};
|
|
240
|
+
const enabled = async (plistDisabled = false) => {
|
|
241
|
+
if (context.platform === 'darwin') {
|
|
242
|
+
const result = await deps.run('launchctl', ['print-disabled', `gui/${context.userId}`]);
|
|
243
|
+
if (!result.stdout.includes('{') || !result.stdout.includes('}'))
|
|
244
|
+
throw new Error('Cannot determine launchd enablement');
|
|
245
|
+
const line = result.stdout.split('\n').find(line => line.includes(`"${SERVICE_LABEL}"`));
|
|
246
|
+
if (line) {
|
|
247
|
+
const override = /=>\s*(true|false|enabled|disabled)\b/.exec(line);
|
|
248
|
+
if (!override)
|
|
249
|
+
throw new Error('Cannot determine launchd enablement');
|
|
250
|
+
return override[1] === 'true' || override[1] === 'disabled' ? false : true;
|
|
231
251
|
}
|
|
232
|
-
return
|
|
252
|
+
return !plistDisabled;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
return (await deps.run('systemctl', ['--user', 'is-enabled', `${SERVICE_LABEL}.service`])).stdout.trim() === 'enabled';
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (/disabled|not-found/.test(String(error)))
|
|
259
|
+
return false;
|
|
260
|
+
throw error;
|
|
233
261
|
}
|
|
234
|
-
return launchdStart();
|
|
235
262
|
};
|
|
236
263
|
// Extracted so `start()` can reuse it: reinstalling is the same work whether triggered
|
|
237
264
|
// explicitly (`axom workstation install`) or as `start()`'s self-heal for a missing definition
|
|
238
265
|
// file. Both platform branches leave the service running, so callers need no separate start.
|
|
239
266
|
const performInstall = async (_options = {}) => {
|
|
267
|
+
const present = await deps.pathExists(target);
|
|
268
|
+
const plistDisabled = context.platform === 'darwin' && present && (await deps.readLaunchAgent(target)).Disabled === true;
|
|
269
|
+
const preserveDisabled = (context.platform === 'darwin' || present) && !await enabled(plistDisabled);
|
|
240
270
|
await deps.assertWritable(context.npmPrefix);
|
|
241
271
|
await deps.ensureDirectory(path.dirname(target));
|
|
242
272
|
const temp = await deps.writeTemp(contents);
|
|
@@ -246,7 +276,8 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
246
276
|
await deps.run('mv', [targetTemp, target]);
|
|
247
277
|
if (context.platform === 'linux') {
|
|
248
278
|
await deps.run('systemctl', ['--user', 'daemon-reload']);
|
|
249
|
-
|
|
279
|
+
if (!preserveDisabled)
|
|
280
|
+
await deps.run('systemctl', ['--user', 'enable', '--now', `${SERVICE_LABEL}.service`]);
|
|
250
281
|
}
|
|
251
282
|
else {
|
|
252
283
|
// Reload an existing service before bootstrapping the replacement into the GUI domain.
|
|
@@ -255,7 +286,8 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
255
286
|
// bounded probe/bootstrap path so Bootstrap failed: 5 is confirmed then kickstarted, and
|
|
256
287
|
// so a launchd that never releases the job is reported instead of leaving the machine
|
|
257
288
|
// with the bootout applied and nothing bootstrapped in its place.
|
|
258
|
-
|
|
289
|
+
if (!preserveDisabled)
|
|
290
|
+
await launchdStart();
|
|
259
291
|
}
|
|
260
292
|
}
|
|
261
293
|
finally {
|
|
@@ -303,6 +335,11 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
303
335
|
*/
|
|
304
336
|
async start() {
|
|
305
337
|
await withGuiSessionHint(async () => {
|
|
338
|
+
// An explicit Start is consent to re-enable; install/update alone preserves OFF.
|
|
339
|
+
if (context.platform === 'darwin')
|
|
340
|
+
await deps.run('launchctl', ['enable', launchdDomain]);
|
|
341
|
+
else
|
|
342
|
+
await deps.run('systemctl', ['--user', 'enable', `${SERVICE_LABEL}.service`]);
|
|
306
343
|
if (!(await deps.pathExists(target)))
|
|
307
344
|
await performInstall({});
|
|
308
345
|
await lifecycle('start');
|
|
@@ -310,6 +347,60 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
310
347
|
},
|
|
311
348
|
async stop() { await withGuiSessionHint(() => lifecycle('stop')); },
|
|
312
349
|
async restart() { await withGuiSessionHint(() => lifecycle('restart')); },
|
|
350
|
+
async setEnabled(value) {
|
|
351
|
+
await withGuiSessionHint(async () => {
|
|
352
|
+
if (context.platform === 'darwin') {
|
|
353
|
+
await deps.run('launchctl', [value ? 'enable' : 'disable', launchdDomain]);
|
|
354
|
+
if (value) {
|
|
355
|
+
if (!await deps.pathExists(target))
|
|
356
|
+
await performInstall({});
|
|
357
|
+
await launchdStart();
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
try {
|
|
361
|
+
await lifecycle('stop');
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (!launchdAbsent(error))
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
await deps.run('systemctl', ['--user', value ? 'enable' : 'disable', '--now', `${SERVICE_LABEL}.service`]);
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
async enablement() {
|
|
374
|
+
const installed = await deps.pathExists(target);
|
|
375
|
+
if (!installed)
|
|
376
|
+
return { installed: false, enabled: false, loaded: false, running: false };
|
|
377
|
+
let value;
|
|
378
|
+
try {
|
|
379
|
+
let disabled = false;
|
|
380
|
+
if (context.platform === 'darwin') {
|
|
381
|
+
const plist = await deps.readLaunchAgent(target);
|
|
382
|
+
const args = plist.ProgramArguments;
|
|
383
|
+
const environment = plist.EnvironmentVariables;
|
|
384
|
+
if (plist.Label !== SERVICE_LABEL || !Array.isArray(args) || args.length !== 3 || args[0] !== context.nodePath || args[1] !== context.cliPath || args[2] !== '__daemon' || environment?.AXOM_WORKSTATION_CONFIG_DIR !== context.configDir) {
|
|
385
|
+
return { installed, enabled: null, loaded: false, running: false, reason: 'service_identity_mismatch' };
|
|
386
|
+
}
|
|
387
|
+
disabled = plist.Disabled === true;
|
|
388
|
+
}
|
|
389
|
+
value = await enabled(disabled);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return { installed, enabled: null, loaded: false, running: false, reason: 'service_state_unknown' };
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const result = await lifecycle('status');
|
|
396
|
+
return { installed, enabled: value, loaded: true, running: context.platform === 'darwin' ? launchdIsRunning(result.stdout) : true };
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (launchdAbsent(error) || /inactive|not loaded|not-found/.test(String(error)))
|
|
400
|
+
return { installed, enabled: value, loaded: false, running: false };
|
|
401
|
+
return { installed, enabled: value, loaded: false, running: false, reason: 'runtime_state_unknown' };
|
|
402
|
+
}
|
|
403
|
+
},
|
|
313
404
|
async status() {
|
|
314
405
|
try {
|
|
315
406
|
const result = await lifecycle('status');
|
|
@@ -31,6 +31,7 @@ ${keyValue('AXOM_WORKSTATION_CONFIG_DIR', context.configDir)}
|
|
|
31
31
|
${keyValue('AXOM_WORKSTATION_NPM_PATH', context.npmPath)}
|
|
32
32
|
${keyValue('AXOM_WORKSTATION_NPM_PREFIX', context.npmPrefix)}
|
|
33
33
|
${keyValue('PATH', context.pathEnv)}
|
|
34
|
+
${context.npmConfigPrefix ? keyValue('NPM_CONFIG_PREFIX', context.npmConfigPrefix) : ''}
|
|
34
35
|
\t</dict>
|
|
35
36
|
\t<key>StandardOutPath</key>
|
|
36
37
|
\t<string>${xml(`${context.configDir}/daemon.log`)}</string>
|
|
@@ -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),
|
|
@@ -261,20 +222,23 @@ export class SystemMetricsSampler {
|
|
|
261
222
|
: (this.deps.platform === 'darwin' ? this.darwinMemoryPressure : null);
|
|
262
223
|
const pools = new Map();
|
|
263
224
|
for (const pool of this.storage) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
225
|
+
for (const cwd of pool.paths) {
|
|
226
|
+
try {
|
|
227
|
+
if (String(this.deps.stat(cwd).dev) !== pool.devices[cwd])
|
|
228
|
+
continue;
|
|
229
|
+
const stats = this.deps.statfs(cwd);
|
|
230
|
+
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
231
|
+
const availableBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
232
|
+
pools.set(pool.key, {
|
|
233
|
+
id: stablePoolId(pool.key),
|
|
234
|
+
label: pool.label,
|
|
235
|
+
workspaces: pool.workspaces,
|
|
236
|
+
totalBytes,
|
|
237
|
+
availableBytes,
|
|
238
|
+
});
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
278
242
|
}
|
|
279
243
|
}
|
|
280
244
|
return {
|
|
@@ -338,43 +302,31 @@ export class SystemMetricsSampler {
|
|
|
338
302
|
this.darwinMemoryPressure = pressure;
|
|
339
303
|
}
|
|
340
304
|
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] });
|
|
305
|
+
try {
|
|
306
|
+
const pools = await this.deps.discoverStorage();
|
|
307
|
+
this.storage = pools.map((pool) => {
|
|
308
|
+
const devices = Object.fromEntries(pool.paths.flatMap((cwd) => {
|
|
309
|
+
try {
|
|
310
|
+
return [[cwd, String(this.deps.stat(cwd).dev)]];
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
const poolDevices = new Set(Object.values(devices));
|
|
317
|
+
const workspaces = this.workspaces.filter(({ cwd }) => {
|
|
318
|
+
try {
|
|
319
|
+
return poolDevices.has(String(this.deps.stat(cwd).dev));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}).map(({ name }) => name);
|
|
325
|
+
return { ...pool, workspaces, devices };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return;
|
|
377
330
|
}
|
|
378
|
-
this.storage = [...grouped.values()];
|
|
379
331
|
}
|
|
380
332
|
}
|
|
@@ -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
|
+
}
|