fix: add windows trans fs read ops
This commit is contained in:
@@ -185,6 +185,14 @@ export function sshHelp(): unknown {
|
||||
"trans D601:win/c/test cmd <<'CMD'",
|
||||
"trans D601:win cmd ver",
|
||||
"trans D601:win/c/test cmd cd",
|
||||
"trans D601:win/c/test pwd",
|
||||
"trans D601:win/c/test ls --limit 50",
|
||||
"trans D601:win/c/test cat README.md",
|
||||
"trans D601:win/c/test head -n 40 README.md",
|
||||
"trans D601:win/c/test tail -n 40 README.md",
|
||||
"trans D601:win/c/test stat README.md",
|
||||
"trans D601:win/c/test wc README.md",
|
||||
"trans D601:win/c/test rg -i needle .",
|
||||
"trans D601:win skills [--scope agents|codex|all] [--limit N]",
|
||||
"trans D601:k3s",
|
||||
"trans D601:k3s kubectl get pods -n hwlab-dev",
|
||||
@@ -207,6 +215,7 @@ export function sshHelp(): unknown {
|
||||
"trans --help and trans <route> --help print this JSON help and never open an interactive session; the underlying ssh subcommand keeps the same help behavior.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and explicit sh/bash stdin for shell logic.",
|
||||
"Windows routes have explicit Windows operations, not POSIX shell aliases: `ps` runs Windows PowerShell from stdin or one inline command, `cmd` runs cmd.exe/batch from stdin or one command line, and `skills` discovers Windows skill directories.",
|
||||
"Windows routes include read-only filesystem convenience operations `pwd`, `ls`, `cat`, `head`, `tail`, `stat`, `wc`, and a bounded UTF-8 `rg` subset. These are implemented through a Windows fs backend with UTF-8/binary checks and bounded output; they do not imply POSIX `sh`/`bash` availability.",
|
||||
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use POSIX `sh` or `bash` for Windows PowerShell.",
|
||||
"For Windows cmd.exe, use `trans <provider>:win/<drive>/<path> cmd <<'CMD'`; `cmd` with no command-line arguments reads the UTF-8 batch body from stdin, injects UTF-8/Python encoding defaults, runs it from a temp .cmd file, and deletes the temp file.",
|
||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `sh -- 'ls -la'` or `bash -- 'ls -la'` for one-line shell logic.",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseSshInvocation,
|
||||
sshStdoutStreamMaxBytes,
|
||||
sshStdoutTruncationHint,
|
||||
windowsFsReadOnlyScript,
|
||||
windowsPowerShellScriptPrelude,
|
||||
} from "./ssh";
|
||||
|
||||
@@ -20,6 +21,44 @@ describe("ssh windows PowerShell safety prelude", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ssh windows fs read-only operations", () => {
|
||||
test("routes common read-only commands through the Windows fs backend", () => {
|
||||
const invocation = parseSshInvocation("D601:win/c/test", ["cat", "hello.md"]);
|
||||
const rgInvocation = parseSshInvocation("D601:win/c/test", ["rg", "-i", "needle", "."]);
|
||||
|
||||
expect(invocation.route.plane).toBe("win");
|
||||
expect(invocation.route.workspace).toBe("C:\\test");
|
||||
expect(invocation.parsed.invocationKind).toBe("helper");
|
||||
expect(invocation.parsed.requiresStdin).toBe(false);
|
||||
expect(invocation.parsed.remoteCommand).toContain("powershell.exe");
|
||||
expect(invocation.parsed.remoteCommand).toContain("-EncodedCommand");
|
||||
expect(rgInvocation.parsed.invocationKind).toBe("helper");
|
||||
expect(rgInvocation.parsed.requiresStdin).toBe(false);
|
||||
});
|
||||
|
||||
test("builds bounded UTF-8 scripts for cat, ls, and rg", () => {
|
||||
const catScript = windowsFsReadOnlyScript("F:\\Work\\demo", "cat", ["--max-bytes", "4096", "中文.md"]);
|
||||
const lsScript = windowsFsReadOnlyScript("F:\\Work\\demo", "ls", ["-la", "--limit=5"]);
|
||||
const rgScript = windowsFsReadOnlyScript("F:\\Work\\demo", "rg", ["-i", "--max-count=5", "needle", "."]);
|
||||
|
||||
expect(catScript).toContain("$operation = 'cat';");
|
||||
expect(catScript).toContain("F:\\Work\\demo");
|
||||
expect(catScript).toContain("binary file refused by windows fs read operation");
|
||||
expect(catScript).toContain("file is not valid UTF-8");
|
||||
expect(catScript).toContain("file too large for windows fs read operation");
|
||||
expect(lsScript).toContain("$operation = 'ls';");
|
||||
expect(lsScript).toContain("UNIDESK_WINDOWS_FS_TRUNCATED");
|
||||
expect(lsScript).toContain("TYPE BYTES UPDATED NAME");
|
||||
expect(rgScript).toContain("$operation = 'rg';");
|
||||
expect(rgScript).toContain("unsupported rg option on Windows route");
|
||||
expect(rgScript).toContain("UNIDESK_WINDOWS_FS_SKIPPED");
|
||||
});
|
||||
|
||||
test("rejects unsupported Windows read operations instead of treating them as POSIX", () => {
|
||||
expect(() => parseSshInvocation("D601:win/c/test", ["sed", "-n", "1p", "hello.md"])).toThrow("unsupported ssh win operation: sed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ssh stdout bounded streaming", () => {
|
||||
test("uses bounded defaults and clamps env override", () => {
|
||||
expect(sshStdoutStreamMaxBytes({} as NodeJS.ProcessEnv)).toBe(256 * 1024);
|
||||
|
||||
+53
-1
@@ -1170,6 +1170,13 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
if (operation === "playwright") {
|
||||
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (isWindowsFsReadOnlyOperation(operation)) {
|
||||
return {
|
||||
remoteCommand: buildWindowsPowerShellInvocation(windowsFsReadOnlyScript(route.workspace, operation, args.slice(1))),
|
||||
requiresStdin: false,
|
||||
invocationKind: "helper",
|
||||
};
|
||||
}
|
||||
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
|
||||
@@ -1194,7 +1201,7 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
};
|
||||
}
|
||||
if (operation !== "cmd" && operation !== "cmd.exe") {
|
||||
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
|
||||
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win <pwd|ls|cat|head|tail|stat|wc|rg>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
|
||||
}
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length === 0) {
|
||||
@@ -1211,6 +1218,51 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
};
|
||||
}
|
||||
|
||||
type WindowsFsReadOnlyOperation = "pwd" | "ls" | "cat" | "head" | "tail" | "stat" | "wc" | "rg";
|
||||
const windowsFsReadOnlyOperations = new Set<string>(["pwd", "ls", "cat", "head", "tail", "stat", "wc", "rg"]);
|
||||
const windowsFsReadOnlyDefaultMaxBytes = 256 * 1024;
|
||||
|
||||
function isWindowsFsReadOnlyOperation(operation: string): operation is WindowsFsReadOnlyOperation {
|
||||
return windowsFsReadOnlyOperations.has(operation);
|
||||
}
|
||||
|
||||
export function windowsFsReadOnlyScript(basePath: string | null, operation: WindowsFsReadOnlyOperation, args: string[]): string {
|
||||
const argsJsonB64 = Buffer.from(JSON.stringify(args), "utf8").toString("base64");
|
||||
const commonScript = [
|
||||
"$ErrorActionPreference = 'Stop';",
|
||||
"$ProgressPreference = 'SilentlyContinue';",
|
||||
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
`$basePath = ${powerShellSingleQuote(basePath ?? "")};`,
|
||||
`$operation = ${powerShellSingleQuote(operation)};`,
|
||||
`$argsJsonB64 = ${powerShellSingleQuote(argsJsonB64)};`,
|
||||
"$toolArgs = @();",
|
||||
"if (-not [string]::IsNullOrEmpty($argsJsonB64)) { $json = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($argsJsonB64)); $decoded = ConvertFrom-Json -InputObject $json; if ($null -ne $decoded) { foreach ($item in @($decoded)) { $toolArgs += [string]$item } } }",
|
||||
"function Fail([string]$Message, [int]$Code) { [Console]::Error.WriteLine($Message); exit $Code }",
|
||||
"function Resolve-UnideskPath([string]$Raw) { if ([string]::IsNullOrWhiteSpace($Raw) -or $Raw -eq '.') { if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath($basePath) }; return (Get-Location).ProviderPath }; if ([System.IO.Path]::IsPathRooted($Raw)) { return [System.IO.Path]::GetFullPath($Raw) }; if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($basePath, $Raw)) }; return [System.IO.Path]::GetFullPath($Raw) }",
|
||||
"function Need-File([string]$Path) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Fail ('file not found: ' + $Path) 1 } }",
|
||||
"function Get-Sha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }",
|
||||
"function Read-StrictText([string]$Path, [Int64]$MaxBytes) { Need-File $Path; $info = [System.IO.FileInfo]$Path; if ($MaxBytes -gt 0 -and $info.Length -gt $MaxBytes) { Fail ('file too large for windows fs read operation: ' + $Path + ' bytes=' + $info.Length + ' maxBytes=' + $MaxBytes + '; use head/tail/download or --max-bytes') 23 }; $bytes = [System.IO.File]::ReadAllBytes($Path); if ([Array]::IndexOf($bytes, [byte]0) -ge 0) { Fail ('binary file refused by windows fs read operation: ' + $Path) 24 }; $utf8 = [System.Text.UTF8Encoding]::new($false, $true); try { return $utf8.GetString($bytes) } catch { Fail ('file is not valid UTF-8: ' + $Path + ': ' + $_.Exception.Message) 25 } }",
|
||||
"function Try-Read-StrictText([string]$Path, [Int64]$MaxBytes) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }; $info = [System.IO.FileInfo]$Path; if ($MaxBytes -gt 0 -and $info.Length -gt $MaxBytes) { return $null }; $bytes = [System.IO.File]::ReadAllBytes($Path); if ([Array]::IndexOf($bytes, [byte]0) -ge 0) { return $null }; $utf8 = [System.Text.UTF8Encoding]::new($false, $true); try { return $utf8.GetString($bytes) } catch { return $null } }",
|
||||
"function Parse-PositiveInt([string]$Name, [string]$Value, [int]$Max) { $parsed = 0; if (-not [Int32]::TryParse($Value, [ref]$parsed) -or $parsed -lt 0 -or ($Max -gt 0 -and $parsed -gt $Max)) { Fail ($Name + ' must be an integer from 0 to ' + $Max) 2 }; return $parsed }",
|
||||
"function Print-Lines([string[]]$Lines) { if ($Lines.Count -gt 0) { [Console]::Out.Write(([string]::Join(\"`n\", $Lines)) + \"`n\") } }",
|
||||
"function Text-Lines([string]$Text) { $lines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($Text -split \"`r?`n\", -1)) { $lines.Add($line) | Out-Null }; if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }; return [string[]]$lines }",
|
||||
"function Path-Args([int]$Start) { $paths = New-Object System.Collections.Generic.List[string]; for ($i = $Start; $i -lt $toolArgs.Count; $i++) { $paths.Add([string]$toolArgs[$i]) | Out-Null }; return [string[]]$paths }",
|
||||
];
|
||||
const operationScript: Record<WindowsFsReadOnlyOperation, string[]> = {
|
||||
pwd: ["if ($toolArgs.Count -ne 0) { Fail 'pwd accepts no arguments on Windows routes' 2 }; [Console]::Out.WriteLine((Resolve-UnideskPath '.'))"],
|
||||
ls: ["$all = $false; $limit = 200; $path = '.'; for ($i = 0; $i -lt $toolArgs.Count; $i++) { $arg = [string]$toolArgs[$i]; if ($arg -eq '-a' -or $arg -eq '--all') { $all = $true; continue }; if ($arg -eq '-l' -or $arg -eq '-la' -or $arg -eq '-al') { if ($arg.Contains('a')) { $all = $true }; continue }; if ($arg -eq '--limit') { $i += 1; if ($i -ge $toolArgs.Count) { Fail '--limit requires a value' 2 }; $limit = Parse-PositiveInt '--limit' ([string]$toolArgs[$i]) 5000; continue }; if ($arg.StartsWith('--limit=')) { $limit = Parse-PositiveInt '--limit' $arg.Substring(8) 5000; continue }; if ($arg.StartsWith('-') -and $arg -ne '-') { Fail ('unsupported ls option on Windows route: ' + $arg) 2 }; $path = $arg }; $target = Resolve-UnideskPath $path; if (-not (Test-Path -LiteralPath $target)) { Fail ('path not found: ' + $target) 1 }; $items = if (Test-Path -LiteralPath $target -PathType Container) { if ($all) { @(Get-ChildItem -LiteralPath $target -Force | Sort-Object -Property Name) } else { @(Get-ChildItem -LiteralPath $target | Sort-Object -Property Name) } } else { @(Get-Item -LiteralPath $target) }; [Console]::Out.WriteLine('TYPE BYTES UPDATED NAME'); $count = 0; foreach ($item in $items) { if ($count -ge $limit) { [Console]::Error.WriteLine('UNIDESK_WINDOWS_FS_TRUNCATED ls limit=' + $limit); break }; $type = if ($item.PSIsContainer) { 'dir' } else { 'file' }; $bytes = if ($item.PSIsContainer) { '-' } else { [string]$item.Length }; [Console]::Out.WriteLine($type + ' ' + $bytes + ' ' + $item.LastWriteTimeUtc.ToString('o') + ' ' + $item.Name); $count += 1 }"],
|
||||
cat: ["$maxBytes = " + String(windowsFsReadOnlyDefaultMaxBytes) + "; $paths = New-Object System.Collections.Generic.List[string]; for ($i = 0; $i -lt $toolArgs.Count; $i++) { $arg = [string]$toolArgs[$i]; if ($arg -eq '--max-bytes') { $i += 1; if ($i -ge $toolArgs.Count) { Fail '--max-bytes requires a value' 2 }; $maxBytes = Parse-PositiveInt '--max-bytes' ([string]$toolArgs[$i]) 16777216; continue }; if ($arg.StartsWith('--max-bytes=')) { $maxBytes = Parse-PositiveInt '--max-bytes' $arg.Substring(12) 16777216; continue }; if ($arg.StartsWith('-') -and $arg -ne '-') { Fail ('unsupported cat option on Windows route: ' + $arg) 2 }; $paths.Add($arg) | Out-Null }; if ($paths.Count -ne 1) { Fail 'cat requires exactly one file path on Windows routes' 2 }; [Console]::Out.Write((Read-StrictText (Resolve-UnideskPath $paths[0]) $maxBytes))"],
|
||||
head: ["$linesWanted = 10; $paths = New-Object System.Collections.Generic.List[string]; for ($i = 0; $i -lt $toolArgs.Count; $i++) { $arg = [string]$toolArgs[$i]; if ($arg -eq '-n' -or $arg -eq '--lines') { $i += 1; if ($i -ge $toolArgs.Count) { Fail ($arg + ' requires a value') 2 }; $linesWanted = Parse-PositiveInt $arg ([string]$toolArgs[$i]) 10000; continue }; if ($arg.StartsWith('-n') -and $arg.Length -gt 2) { $linesWanted = Parse-PositiveInt '-n' $arg.Substring(2) 10000; continue }; if ($arg.StartsWith('--lines=')) { $linesWanted = Parse-PositiveInt '--lines' $arg.Substring(8) 10000; continue }; if ($arg.StartsWith('-') -and $arg -ne '-') { Fail ('unsupported head option on Windows route: ' + $arg) 2 }; $paths.Add($arg) | Out-Null }; if ($paths.Count -ne 1) { Fail 'head requires exactly one file path on Windows routes' 2 }; $lines = Text-Lines (Read-StrictText (Resolve-UnideskPath $paths[0]) 1048576); Print-Lines ([string[]]($lines | Select-Object -First $linesWanted))"],
|
||||
tail: ["$linesWanted = 10; $paths = New-Object System.Collections.Generic.List[string]; for ($i = 0; $i -lt $toolArgs.Count; $i++) { $arg = [string]$toolArgs[$i]; if ($arg -eq '-n' -or $arg -eq '--lines') { $i += 1; if ($i -ge $toolArgs.Count) { Fail ($arg + ' requires a value') 2 }; $linesWanted = Parse-PositiveInt $arg ([string]$toolArgs[$i]) 10000; continue }; if ($arg.StartsWith('-n') -and $arg.Length -gt 2) { $linesWanted = Parse-PositiveInt '-n' $arg.Substring(2) 10000; continue }; if ($arg.StartsWith('--lines=')) { $linesWanted = Parse-PositiveInt '--lines' $arg.Substring(8) 10000; continue }; if ($arg.StartsWith('-') -and $arg -ne '-') { Fail ('unsupported tail option on Windows route: ' + $arg) 2 }; $paths.Add($arg) | Out-Null }; if ($paths.Count -ne 1) { Fail 'tail requires exactly one file path on Windows routes' 2 }; $lines = Text-Lines (Read-StrictText (Resolve-UnideskPath $paths[0]) 1048576); Print-Lines ([string[]]($lines | Select-Object -Last $linesWanted))"],
|
||||
rg: ["$ignoreCase = $false; $fixed = $false; $maxCount = 1000; $maxFiles = 5000; $maxBytes = 1048576; $pattern = $null; $paths = New-Object System.Collections.Generic.List[string]; $endOptions = $false; for ($i = 0; $i -lt $toolArgs.Count; $i++) { $arg = [string]$toolArgs[$i]; if ($null -eq $pattern -and -not $endOptions -and $arg -eq '--') { $endOptions = $true; continue }; if ($null -eq $pattern -and -not $endOptions -and ($arg -eq '-i' -or $arg -eq '--ignore-case')) { $ignoreCase = $true; continue }; if ($null -eq $pattern -and -not $endOptions -and ($arg -eq '-F' -or $arg -eq '--fixed-strings')) { $fixed = $true; continue }; if ($null -eq $pattern -and -not $endOptions -and ($arg -eq '-n' -or $arg -eq '--line-number')) { continue }; if ($null -eq $pattern -and -not $endOptions -and ($arg -eq '-m' -or $arg -eq '--max-count')) { $i += 1; if ($i -ge $toolArgs.Count) { Fail ($arg + ' requires a value') 2 }; $maxCount = Parse-PositiveInt $arg ([string]$toolArgs[$i]) 10000; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg.StartsWith('--max-count=')) { $maxCount = Parse-PositiveInt '--max-count' $arg.Substring(12) 10000; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg.StartsWith('-m') -and $arg.Length -gt 2) { $maxCount = Parse-PositiveInt '-m' $arg.Substring(2) 10000; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg -eq '--max-files') { $i += 1; if ($i -ge $toolArgs.Count) { Fail '--max-files requires a value' 2 }; $maxFiles = Parse-PositiveInt '--max-files' ([string]$toolArgs[$i]) 50000; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg.StartsWith('--max-files=')) { $maxFiles = Parse-PositiveInt '--max-files' $arg.Substring(12) 50000; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg -eq '--max-bytes') { $i += 1; if ($i -ge $toolArgs.Count) { Fail '--max-bytes requires a value' 2 }; $maxBytes = Parse-PositiveInt '--max-bytes' ([string]$toolArgs[$i]) 16777216; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg.StartsWith('--max-bytes=')) { $maxBytes = Parse-PositiveInt '--max-bytes' $arg.Substring(12) 16777216; continue }; if ($null -eq $pattern -and -not $endOptions -and $arg.StartsWith('-')) { Fail ('unsupported rg option on Windows route: ' + $arg) 2 }; if ($null -eq $pattern) { $pattern = $arg } else { $paths.Add($arg) | Out-Null } }; if ($null -eq $pattern) { Fail 'rg requires a pattern on Windows routes' 2 }; if ($paths.Count -eq 0) { $paths.Add('.') | Out-Null }; $regexPattern = if ($fixed) { [regex]::Escape($pattern) } else { $pattern }; $regexOptions = [System.Text.RegularExpressions.RegexOptions]::CultureInvariant; if ($ignoreCase) { $regexOptions = $regexOptions -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase }; try { $regex = [regex]::new($regexPattern, $regexOptions) } catch { Fail ('invalid rg pattern on Windows route: ' + $_.Exception.Message) 2 }; $files = New-Object System.Collections.Generic.List[string]; $fileTruncated = $false; foreach ($raw in $paths) { $target = Resolve-UnideskPath $raw; if (Test-Path -LiteralPath $target -PathType Leaf) { if ($files.Count -lt $maxFiles) { $files.Add($target) | Out-Null } else { $fileTruncated = $true; break } } elseif (Test-Path -LiteralPath $target -PathType Container) { foreach ($child in Get-ChildItem -LiteralPath $target -Recurse -File -ErrorAction SilentlyContinue) { if ($files.Count -ge $maxFiles) { $fileTruncated = $true; break }; $files.Add($child.FullName) | Out-Null } } else { Fail ('path not found: ' + $target) 1 }; if ($fileTruncated) { break } }; if ($fileTruncated) { [Console]::Error.WriteLine('UNIDESK_WINDOWS_FS_TRUNCATED rg maxFiles=' + $maxFiles) }; $matchCount = 0; $skipped = 0; $truncated = $false; foreach ($file in $files) { $text = Try-Read-StrictText $file $maxBytes; if ($null -eq $text) { $skipped += 1; continue }; $lines = Text-Lines $text; for ($lineIndex = 0; $lineIndex -lt $lines.Count; $lineIndex += 1) { if ($regex.IsMatch($lines[$lineIndex])) { [Console]::Out.WriteLine($file + ':' + ($lineIndex + 1) + ':' + $lines[$lineIndex]); $matchCount += 1; if ($matchCount -ge $maxCount) { $truncated = $true; break } } }; if ($truncated) { break } }; if ($skipped -gt 0) { [Console]::Error.WriteLine('UNIDESK_WINDOWS_FS_SKIPPED rg files=' + $skipped + ' reason=binary-invalid-utf8-or-too-large') }; if ($truncated) { [Console]::Error.WriteLine('UNIDESK_WINDOWS_FS_TRUNCATED rg maxCount=' + $maxCount) }; if ($matchCount -eq 0) { exit 1 }"],
|
||||
stat: ["$paths = Path-Args 0; if ($paths.Count -eq 0) { Fail 'stat requires at least one path on Windows routes' 2 }; [Console]::Out.WriteLine('TYPE BYTES SHA256 PATH'); foreach ($raw in $paths) { $path = Resolve-UnideskPath $raw; if (-not (Test-Path -LiteralPath $path)) { Fail ('path not found: ' + $path) 1 }; $item = Get-Item -LiteralPath $path; if ($item.PSIsContainer) { [Console]::Out.WriteLine('dir - - ' + $path) } else { [Console]::Out.WriteLine('file ' + $item.Length + ' ' + (Get-Sha256 $path) + ' ' + $path) } }"],
|
||||
wc: ["$paths = Path-Args 0; if ($paths.Count -eq 0) { Fail 'wc requires at least one file path on Windows routes' 2 }; [Console]::Out.WriteLine('LINES WORDS CHARS BYTES PATH'); foreach ($raw in $paths) { $path = Resolve-UnideskPath $raw; $text = Read-StrictText $path 1048576; $bytes = ([System.IO.FileInfo]$path).Length; $lineCount = [regex]::Matches($text, \"`n\").Count; if ($text.Length -gt 0 -and -not $text.EndsWith(\"`n\")) { $lineCount += 1 }; $wordCount = [regex]::Matches($text, '\\S+').Count; [Console]::Out.WriteLine(([string]$lineCount) + ' ' + $wordCount + ' ' + $text.Length + ' ' + $bytes + ' ' + $path) }"],
|
||||
};
|
||||
return [...commonScript, ...operationScript[operation]].join(" ");
|
||||
}
|
||||
|
||||
function buildWindowsCmdLine(userCommand: string, cwd: string | null): string {
|
||||
const parts = [
|
||||
"chcp 65001>nul",
|
||||
|
||||
Reference in New Issue
Block a user