@remcp/runtime 0.2.218 → 0.2.221
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/package.json +3 -2
- package/src/extended/desktop-core.mjs +5 -0
- package/src/extended/desktop-windows.mjs +19 -5
- package/src/extended/documents.mjs +39 -4
- package/src/privilege-boundary.mjs +1 -1
- package/src/tools/files-screenshots.mjs +40 -7
- package/src/tools/system-telemetry.mjs +61 -6
- package/src/tools/system.mjs +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.221",
|
|
4
4
|
"description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@jellybrick/dbus-next": "^0.11.3",
|
|
28
|
-
"@modelcontextprotocol/server": "^2.3.0"
|
|
28
|
+
"@modelcontextprotocol/server": "^2.3.0",
|
|
29
|
+
"pdf-lib": "^1.17.1"
|
|
29
30
|
},
|
|
30
31
|
"optionalDependencies": {
|
|
31
32
|
"macos-temperature-sensor": "2.1.1"
|
|
@@ -405,6 +405,11 @@ export async function screenshotRegion(args = {}) {
|
|
|
405
405
|
}
|
|
406
406
|
if (!row) throw new Error('No matching window found for screenshot_region');
|
|
407
407
|
x = Number(row.x); y = Number(row.y); width = Number(row.width); height = Number(row.height);
|
|
408
|
+
} else if (process.platform === 'win32' && optionalString(args.monitor)?.toLowerCase() === 'primary'
|
|
409
|
+
&& args.monitor_index == null && clamp(args.padding, 0, 0, 200) === 0) {
|
|
410
|
+
// Windows GDI capture resolves primary geometry in the same PowerShell
|
|
411
|
+
// invocation. Avoid a second, expensive process on loaded Windows VMs.
|
|
412
|
+
return adapter.screenshotRegion({ monitor:'primary' });
|
|
408
413
|
} else if (args.monitor != null || args.monitor_index != null) {
|
|
409
414
|
const displays = resultValue(await displayInventory());
|
|
410
415
|
if (!Array.isArray(displays)) throw new Error('This platform does not expose normalized monitor geometry for screenshot_region');
|
|
@@ -409,16 +409,30 @@ public static class RMDisplay{
|
|
|
409
409
|
return jsonResult(Array.isArray(parsed)?parsed:[parsed]);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
export function windowsRegionCaptureScript({primary=false,x=0,y=0,width=0,height=0} = {}, target='') {
|
|
413
|
+
const safe=escapePowerShellSingle(target);
|
|
414
|
+
const bounds=primary
|
|
415
|
+
? 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing;$r=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds;'
|
|
416
|
+
: `Add-Type -AssemblyName System.Drawing;$r=[pscustomobject]@{X=${x};Y=${y};Width=${width};Height=${height}};`;
|
|
417
|
+
return `$ErrorActionPreference='Stop';${bounds}if($r.Width -le 0 -or $r.Height -le 0 -or $r.Width -gt 8192 -or $r.Height -gt 8192 -or ($r.Width*$r.Height) -gt 16777216){throw 'Invalid or oversized display bounds'};$b=New-Object System.Drawing.Bitmap $r.Width,$r.Height;$g=$null;try{$g=[System.Drawing.Graphics]::FromImage($b);$g.CopyFromScreen($r.X,$r.Y,0,0,$b.Size);$b.Save('${safe}',[System.Drawing.Imaging.ImageFormat]::Png)}finally{if($g){$g.Dispose()};$b.Dispose()}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
412
420
|
export async function screenshotRegion(args = {}) {
|
|
421
|
+
const primary=args.monitor==='primary' && args.x==null && args.y==null
|
|
422
|
+
&& args.width==null && args.height==null;
|
|
413
423
|
const x=Math.trunc(Number(args.x)),y=Math.trunc(Number(args.y)),width=Math.trunc(Number(args.width)),height=Math.trunc(Number(args.height));
|
|
414
|
-
if(![x,y,width,height].every(Number.isFinite)||width<=0||height<=0)
|
|
415
|
-
|
|
424
|
+
if(!primary && (![x,y,width,height].every(Number.isFinite)||width<=0||height<=0))
|
|
425
|
+
throw new Error('x, y, width and height are required; width/height must be positive');
|
|
426
|
+
if(!primary && (width>8192||height>8192||width*height>16_777_216))
|
|
427
|
+
throw new Error('screenshot region is too large; width/height must be <= 8192 and area <= 16 megapixels');
|
|
416
428
|
const dir=await tempDir('remcp-region-'),target=path.join(dir,'region.png');
|
|
417
429
|
try{
|
|
418
|
-
const
|
|
419
|
-
await runPowerShell(
|
|
430
|
+
const script=windowsRegionCaptureScript({primary,x,y,width,height},target);
|
|
431
|
+
await runPowerShell(script,{label:'screenshot region',timeout:30_000});
|
|
420
432
|
const {data}=await readPrivateTempFile(target,4*1024*1024);
|
|
421
|
-
|
|
433
|
+
const description=primary ? `Captured primary display ${data.readUInt32BE(16)}x${data.readUInt32BE(20)}.`
|
|
434
|
+
: `Captured ${width}x${height} at ${x},${y}.`;
|
|
435
|
+
return multi([{type:'text',text:description},image(data.toString('base64'),'image/png')]);
|
|
422
436
|
}finally{await removeTemp(dir);}
|
|
423
437
|
}
|
|
424
438
|
|
|
@@ -275,6 +275,11 @@ function pdfAnnotations(buffer) {
|
|
|
275
275
|
return rows;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
async function loadPdfLibrary() {
|
|
279
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
280
|
+
return PDFDocument;
|
|
281
|
+
}
|
|
282
|
+
|
|
278
283
|
export async function pdfAction(args) {
|
|
279
284
|
const action = requireEnum(args.action,'action',['merge','split','extract_pages','annotations','info']);
|
|
280
285
|
if (action === 'annotations') {
|
|
@@ -289,7 +294,9 @@ export async function pdfAction(args) {
|
|
|
289
294
|
finally { await removeTemp(snapshot.dir); }
|
|
290
295
|
}
|
|
291
296
|
const data = await readDocumentFile(source);
|
|
292
|
-
|
|
297
|
+
let pageCount = null;
|
|
298
|
+
try { const PDFDocument = await loadPdfLibrary(); pageCount = (await PDFDocument.load(data, { updateMetadata:false })).getPageCount(); } catch {}
|
|
299
|
+
return jsonResult({ path:source, bytes:data.length, ...(pageCount === null ? {} : { page_count:pageCount }), annotation_count:pdfAnnotations(data).length });
|
|
293
300
|
}
|
|
294
301
|
if (action === 'merge') {
|
|
295
302
|
const requested = Array.isArray(args.paths) ? args.paths : [];
|
|
@@ -310,7 +317,15 @@ export async function pdfAction(args) {
|
|
|
310
317
|
}
|
|
311
318
|
if(commandExists('pdfunite')) await runFile('pdfunite',[...snapshots,staged],{label:'PDF merge',timeout:60_000});
|
|
312
319
|
else if(commandExists('qpdf')) await runFile('qpdf',['--empty','--pages',...snapshots,'--',staged],{label:'PDF merge',timeout:60_000});
|
|
313
|
-
else
|
|
320
|
+
else {
|
|
321
|
+
const PDFDocument=await loadPdfLibrary();
|
|
322
|
+
const merged=await PDFDocument.create();
|
|
323
|
+
for(const snapshot of snapshots){
|
|
324
|
+
const sourceDoc=await PDFDocument.load(await readFile(snapshot),{updateMetadata:false});
|
|
325
|
+
for(const page of await merged.copyPages(sourceDoc,sourceDoc.getPageIndices())) merged.addPage(page);
|
|
326
|
+
}
|
|
327
|
+
await writeFile(staged,await merged.save());
|
|
328
|
+
}
|
|
314
329
|
await commitDocumentOutput(staged,output);
|
|
315
330
|
return jsonResult({action,output,inputs:sources.length,bytes:(await stat(output)).size});
|
|
316
331
|
} finally {
|
|
@@ -331,7 +346,20 @@ export async function pdfAction(args) {
|
|
|
331
346
|
try {
|
|
332
347
|
const pattern=path.join(work,patternName);
|
|
333
348
|
if(commandExists('pdfseparate')) await runFile('pdfseparate',[snapshot.path,pattern],{label:'PDF split',timeout:60_000});
|
|
334
|
-
else
|
|
349
|
+
else {
|
|
350
|
+
const PDFDocument=await loadPdfLibrary();
|
|
351
|
+
const sourceDoc=await PDFDocument.load(await readFile(snapshot.path),{updateMetadata:false});
|
|
352
|
+
const total=sourceDoc.getPageCount();
|
|
353
|
+
for(let page=1;page<=total;page+=1){
|
|
354
|
+
const single=await PDFDocument.create();
|
|
355
|
+
const [copied]=await single.copyPages(sourceDoc,[page-1]);
|
|
356
|
+
single.addPage(copied);
|
|
357
|
+
const name=patternName.includes('%d')
|
|
358
|
+
? patternName.replace(/%d/g,String(page))
|
|
359
|
+
: patternName.toLowerCase().endsWith('.pdf') ? `${patternName.slice(0,-4)}-${page}.pdf` : `${patternName}-${page}.pdf`;
|
|
360
|
+
await writeFile(path.join(work,name),await single.save());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
335
363
|
for(const name of await readdir(work)){
|
|
336
364
|
if(!name.toLowerCase().endsWith('.pdf')) continue;
|
|
337
365
|
const staged=path.join(stageDir,name);
|
|
@@ -378,7 +406,14 @@ export async function pdfAction(args) {
|
|
|
378
406
|
}
|
|
379
407
|
} finally { await removeTemp(work); }
|
|
380
408
|
} else {
|
|
381
|
-
|
|
409
|
+
const PDFDocument=await loadPdfLibrary();
|
|
410
|
+
const sourceDoc=await PDFDocument.load(await readFile(snapshot.path),{updateMetadata:false});
|
|
411
|
+
const total=sourceDoc.getPageCount();
|
|
412
|
+
const indices=selectedPages.map(page=>page-1);
|
|
413
|
+
if(indices.some(index=>index>=total)) throw new Error(`Page selection exceeds ${total} pages`);
|
|
414
|
+
const extracted=await PDFDocument.create();
|
|
415
|
+
for(const page of await extracted.copyPages(sourceDoc,indices)) extracted.addPage(page);
|
|
416
|
+
await writeFile(staged,await extracted.save());
|
|
382
417
|
}
|
|
383
418
|
await commitDocumentOutput(staged,output);
|
|
384
419
|
return jsonResult({action,source,pages:normalizedPages,output,bytes:(await stat(output)).size});
|
|
@@ -15,7 +15,7 @@ export function privilegeBoundary({
|
|
|
15
15
|
if (output === undefined) {
|
|
16
16
|
try {
|
|
17
17
|
const result = spawnSync('whoami.exe', ['/groups'], {
|
|
18
|
-
encoding: 'utf8', windowsHide: true, timeout:
|
|
18
|
+
encoding: 'utf8', windowsHide: true, timeout: 5000, maxBuffer: 64 * 1024,
|
|
19
19
|
});
|
|
20
20
|
output = result.status === 0 && !result.error ? result.stdout : '';
|
|
21
21
|
} catch { output = ''; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
4
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
6
|
import { constants, createReadStream } from 'node:fs';
|
|
6
7
|
import { access, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
@@ -21,6 +22,9 @@ import { diffFilesTool, editBlockTool, moveToTrashTool, readBinaryTool, replaceI
|
|
|
21
22
|
import { applyPatchTool, copyFileTool, copyPathsTool, createDirectoryTool, deletePathTool, deletePathsTool, moveFileTool, movePathsTool, pathExists, readFilesTool, setPermissionsTool, writeFilesTool } from './files-bulk-operations.mjs';
|
|
22
23
|
import { createArchiveTool, extractArchiveTool } from './files-archive-actions.mjs';
|
|
23
24
|
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
let windowsScreenshotInFlight = false;
|
|
27
|
+
|
|
24
28
|
export const SCREENSHOT_COMMANDS = [
|
|
25
29
|
{ command: 'grim', args: file => [file] },
|
|
26
30
|
{ command: 'gnome-screenshot', args: file => ['-f', file] },
|
|
@@ -38,6 +42,9 @@ export function screenshotAdvice(attempts, { platform = process.platform, env =
|
|
|
38
42
|
return `macOS refused the screen capture (${attempts.join('; ') || 'no capture command ran'}). Grant Screen Recording to the binary that runs the tools — System Settings → Privacy & Security → Screen Recording → + → ${execPath} — then restart the agent with \`remcp start\`. macOS requires it for screencapture even when the file itself is writable.`;
|
|
39
43
|
}
|
|
40
44
|
if (platform === 'win32') {
|
|
45
|
+
if (attempts.some(attempt => attempt.includes('A Windows screen capture is already in progress'))) {
|
|
46
|
+
return 'A Windows screen capture is already in progress. Retry after it finishes.';
|
|
47
|
+
}
|
|
41
48
|
return `Windows refused the screen capture (${attempts.join('; ') || 'no capture command ran'}). Screen capture needs an interactive desktop session: a machine where nobody is signed in, or a locked session, cannot be captured. Sign in on that computer and try again.`;
|
|
42
49
|
}
|
|
43
50
|
const wayland = /wayland/i.test(String(env.XDG_SESSION_TYPE || '')) || Boolean(env.WAYLAND_DISPLAY);
|
|
@@ -58,13 +65,36 @@ export function screenshotAdvice(attempts, { platform = process.platform, env =
|
|
|
58
65
|
|
|
59
66
|
export function windowsScreenshotScript(file) {
|
|
60
67
|
return [
|
|
68
|
+
"$ErrorActionPreference = 'Stop'",
|
|
61
69
|
'Add-Type -AssemblyName System.Windows.Forms,System.Drawing',
|
|
62
70
|
'$b = [System.Windows.Forms.SystemInformation]::VirtualScreen',
|
|
63
|
-
|
|
64
|
-
'$
|
|
65
|
-
'$g
|
|
71
|
+
"if ($b.Width -le 0 -or $b.Height -le 0) { throw 'No valid desktop geometry for screenshot' }",
|
|
72
|
+
'$bmp = $null',
|
|
73
|
+
'$g = $null',
|
|
74
|
+
'try {',
|
|
75
|
+
' $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height',
|
|
76
|
+
' $g = [System.Drawing.Graphics]::FromImage($bmp)',
|
|
77
|
+
' $g.CopyFromScreen($b.Left, $b.Top, 0, 0, $bmp.Size)',
|
|
66
78
|
`$bmp.Save('${file.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
67
|
-
|
|
79
|
+
'} finally {',
|
|
80
|
+
' if ($null -ne $g) { $g.Dispose() }',
|
|
81
|
+
' if ($null -ne $bmp) { $bmp.Dispose() }',
|
|
82
|
+
'}',
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Never block the runtime event loop while Windows initializes GDI/PowerShell.
|
|
87
|
+
// A slow or inaccessible interactive desktop must not starve ping or relay RPC.
|
|
88
|
+
export async function runWindowsScreenshot(file, runProcess = execFileAsync) {
|
|
89
|
+
if (windowsScreenshotInFlight) throw new Error('A Windows screen capture is already in progress; retry after it finishes');
|
|
90
|
+
windowsScreenshotInFlight = true;
|
|
91
|
+
try {
|
|
92
|
+
return await runProcess('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsScreenshotScript(file)], {
|
|
93
|
+
encoding:'utf8', timeout:30_000, windowsHide:true, maxBuffer:64*1024,
|
|
94
|
+
});
|
|
95
|
+
} finally {
|
|
96
|
+
windowsScreenshotInFlight = false;
|
|
97
|
+
}
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
export async function takeScreenshotTool(args) {
|
|
@@ -86,8 +116,11 @@ export async function takeScreenshotTool(args) {
|
|
|
86
116
|
let preserveCapture = false;
|
|
87
117
|
try {
|
|
88
118
|
if (process.platform === 'win32') {
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
try {
|
|
120
|
+
await runWindowsScreenshot(file);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
attempts.push(`powershell: ${String(error?.stderr || error?.message || error).trim().slice(0, 512)}`);
|
|
123
|
+
}
|
|
91
124
|
} else {
|
|
92
125
|
if (process.platform === 'linux' && isWaylandSession()) {
|
|
93
126
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Best-effort host measurements. Keep interface names, mount paths and sensor labels bounded;
|
|
2
2
|
// never expose raw process arguments or arbitrary kernel sensor metadata.
|
|
3
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { readFile, readdir, statfs } from 'node:fs/promises';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import os from 'node:os';
|
|
@@ -75,6 +75,25 @@ export function parseWindowsNetworkCounters(input, activeNames = null) {
|
|
|
75
75
|
return [...found.values()];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// netsh exposes cumulative interface bytes even on Windows hosts where the
|
|
79
|
+
// NetAdapterStatistics PowerShell provider returns no entries. Match columns
|
|
80
|
+
// rather than localized header words; reject unsafe aliases and fake rates.
|
|
81
|
+
export function parseWindowsNetshCounters(input, activeNames = null) {
|
|
82
|
+
const found = new Map();
|
|
83
|
+
for (const line of String(input).split(/\r?\n/).slice(0, 130)) {
|
|
84
|
+
const match = line.match(/^\s*\d{1,10}\s+1\s+(\d{1,16})\s+(\d{1,16})\s+(.+?)\s*$/u);
|
|
85
|
+
if (!match) continue;
|
|
86
|
+
const name = match[3];
|
|
87
|
+
if (!/^[\p{L}\p{N} _().:#-]{1,64}$/u.test(name) ||
|
|
88
|
+
(activeNames && !activeNames.has(name))) continue;
|
|
89
|
+
const rx = Number(match[1]), tx = Number(match[2]);
|
|
90
|
+
if (!numeric(rx) || !numeric(tx)) continue;
|
|
91
|
+
found.set(name, { name, rx, tx });
|
|
92
|
+
if (found.size >= 24) break;
|
|
93
|
+
}
|
|
94
|
+
return [...found.values()];
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
export function calculateNetworkRates(current, previous, elapsedSeconds) {
|
|
79
98
|
if (!Array.isArray(current) || !Array.isArray(previous) || !Number.isFinite(elapsedSeconds) ||
|
|
80
99
|
elapsedSeconds < 1 || elapsedSeconds > 300) return null;
|
|
@@ -114,13 +133,21 @@ async function readNetworkCounters() {
|
|
|
114
133
|
return parseDarwinNetworkCounters(stdout, active);
|
|
115
134
|
}
|
|
116
135
|
if (process.platform === 'win32') {
|
|
117
|
-
const script = 'Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress';
|
|
118
|
-
const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script],
|
|
119
|
-
{ windowsHide: true, timeout: 5000, maxBuffer: 128 * 1024 });
|
|
120
136
|
const active = new Set(Object.entries(os.networkInterfaces())
|
|
121
137
|
.filter(([, addresses]) => addresses?.some(address => !address.internal))
|
|
122
138
|
.map(([name]) => name));
|
|
123
|
-
|
|
139
|
+
try {
|
|
140
|
+
const { stdout } = await run('netsh', ['interface', 'ipv4', 'show', 'subinterfaces'],
|
|
141
|
+
{ windowsHide: true, timeout: 2500, maxBuffer: 128 * 1024 });
|
|
142
|
+
const counters = parseWindowsNetshCounters(stdout, active);
|
|
143
|
+
if (counters.length) return counters;
|
|
144
|
+
} catch { /* Fall through to the existing adapter provider. */ }
|
|
145
|
+
try {
|
|
146
|
+
const script = 'Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress';
|
|
147
|
+
const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script],
|
|
148
|
+
{ windowsHide: true, timeout: 5000, maxBuffer: 128 * 1024 });
|
|
149
|
+
return parseWindowsNetworkCounters(stdout, active);
|
|
150
|
+
} catch { return []; }
|
|
124
151
|
}
|
|
125
152
|
return [];
|
|
126
153
|
}
|
|
@@ -185,8 +212,36 @@ export function parseDiskVolumes(input) {
|
|
|
185
212
|
return volumes;
|
|
186
213
|
}
|
|
187
214
|
|
|
215
|
+
async function windowsDiskVolumes() {
|
|
216
|
+
const volumes = [];
|
|
217
|
+
for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ') {
|
|
218
|
+
const mount = `${letter}:\\`;
|
|
219
|
+
let timer;
|
|
220
|
+
try {
|
|
221
|
+
const info = await Promise.race([
|
|
222
|
+
statfs(mount),
|
|
223
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('volume probe timeout')), 250); }),
|
|
224
|
+
]);
|
|
225
|
+
const totalBytes = Number(info.blocks) * Number(info.bsize);
|
|
226
|
+
const freeBytes = Number(info.bfree) * Number(info.bsize);
|
|
227
|
+
const availableBytes = Number(info.bavail) * Number(info.bsize);
|
|
228
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) continue;
|
|
229
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
230
|
+
volumes.push({
|
|
231
|
+
mount,
|
|
232
|
+
totalBytes,
|
|
233
|
+
usedBytes,
|
|
234
|
+
availableBytes,
|
|
235
|
+
usedRatio: Number((usedBytes / totalBytes).toFixed(3)),
|
|
236
|
+
});
|
|
237
|
+
} catch {}
|
|
238
|
+
finally { clearTimeout(timer); }
|
|
239
|
+
}
|
|
240
|
+
return volumes;
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
export async function diskVolumes() {
|
|
189
|
-
if (process.platform === 'win32') return
|
|
244
|
+
if (process.platform === 'win32') return windowsDiskVolumes();
|
|
190
245
|
try {
|
|
191
246
|
const { stdout } = await run('df', ['-kP'], { timeout: 2000, maxBuffer: 256 * 1024 });
|
|
192
247
|
return parseDiskVolumes(stdout);
|
package/src/tools/system.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import process from 'node:process';
|
|
3
4
|
import { execFile } from 'node:child_process';
|
|
5
|
+
import { statfs } from 'node:fs/promises';
|
|
4
6
|
import { promisify } from 'node:util';
|
|
5
7
|
import { runtimeConfig } from '../config.mjs';
|
|
6
8
|
import { clampInteger, fail, requireInteger, text } from '../util.mjs';
|
|
@@ -18,7 +20,23 @@ export function redactSecrets(command) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
async function diskUsage() {
|
|
21
|
-
if (process.platform === 'win32')
|
|
23
|
+
if (process.platform === 'win32') {
|
|
24
|
+
const mount = path.parse(process.cwd()).root || 'C:\\';
|
|
25
|
+
let timer;
|
|
26
|
+
try {
|
|
27
|
+
const info = await Promise.race([
|
|
28
|
+
statfs(mount),
|
|
29
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('disk probe timeout')), 500); }),
|
|
30
|
+
]);
|
|
31
|
+
const totalBytes = Number(info.blocks) * Number(info.bsize);
|
|
32
|
+
const freeBytes = Number(info.bfree) * Number(info.bsize);
|
|
33
|
+
const availableBytes = Number(info.bavail) * Number(info.bsize);
|
|
34
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) return null;
|
|
35
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
36
|
+
return { mount, totalBytes, usedBytes, availableBytes, usedRatio: Number((usedBytes / totalBytes).toFixed(3)) };
|
|
37
|
+
} catch { return null; }
|
|
38
|
+
finally { clearTimeout(timer); }
|
|
39
|
+
}
|
|
22
40
|
try {
|
|
23
41
|
const { stdout } = await run('df', ['-kP', process.cwd()], { maxBuffer: 1024 * 1024 });
|
|
24
42
|
const line = stdout.trim().split('\n')[1];
|