fix: harden Windows trans helpers
Resolve #1691 by preserving argv boundaries, adding bounded native rg and wc/skill query support, surfacing WSL-to-Windows hints, and splitting the oversized SSH module and embedded remote scripts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
allow_loose=0
|
||||
|
||||
die() {
|
||||
printf 'apply_patch: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mk_temp() {
|
||||
mktemp "${TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1
|
||||
}
|
||||
|
||||
ensure_parent() {
|
||||
case "$1" in
|
||||
*/*)
|
||||
dir=${1%/*}
|
||||
[ -n "$dir" ] || dir=/
|
||||
mkdir -p "$dir"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
read_text_preserve_newlines() {
|
||||
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
|
||||
text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1"
|
||||
printf '%s' "${text%"$marker"}"
|
||||
}
|
||||
|
||||
write_file() {
|
||||
target=$1
|
||||
source=$2
|
||||
ensure_parent "$target"
|
||||
cp "$source" "$target" || die "failed to write $target"
|
||||
}
|
||||
|
||||
line_number_for_prefix() {
|
||||
newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ')
|
||||
printf '%s\n' $((newline_count + 1))
|
||||
}
|
||||
|
||||
replace_once_with_perl() {
|
||||
command -v perl >/dev/null 2>&1 || return 127
|
||||
perl -0777 -e '
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub fail {
|
||||
print STDERR "apply_patch: ", @_, "\n";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
sub read_all {
|
||||
my ($path, $label) = @_;
|
||||
open my $fh, "<", $path or fail("failed to read $label");
|
||||
binmode $fh;
|
||||
local $/;
|
||||
my $data = <$fh>;
|
||||
close $fh;
|
||||
return defined $data ? $data : "";
|
||||
}
|
||||
|
||||
my ($target, $search_file, $replace_file, $hunk_id, $allow_loose, $out) = @ARGV;
|
||||
my $old = read_all($target, $target);
|
||||
my $search = read_all($search_file, "hunk search");
|
||||
my $replacement = read_all($replace_file, "hunk replacement");
|
||||
my $new;
|
||||
|
||||
if ($search eq "") {
|
||||
fail("hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review") if $allow_loose ne "1";
|
||||
print STDERR "apply_patch: hunk $hunk_id matched $target:1 (loose)\n";
|
||||
$new = $replacement . $old;
|
||||
} else {
|
||||
my $pos = -1;
|
||||
my $offset = 0;
|
||||
my $count = 0;
|
||||
while (1) {
|
||||
my $found = index($old, $search, $offset);
|
||||
last if $found < 0;
|
||||
$pos = $found if $count == 0;
|
||||
$count += 1;
|
||||
last if $count > 1 && $allow_loose ne "1";
|
||||
$offset = $found + length($search);
|
||||
}
|
||||
fail("hunk $hunk_id context not found in $target") if $count == 0;
|
||||
fail("hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review") if $count > 1 && $allow_loose ne "1";
|
||||
my $prefix = substr($old, 0, $pos);
|
||||
my $suffix = substr($old, $pos + length($search));
|
||||
my $line = ($prefix =~ tr/\n//) + 1;
|
||||
print STDERR "apply_patch: hunk $hunk_id matched $target:$line\n";
|
||||
$new = $prefix . $replacement . $suffix;
|
||||
}
|
||||
|
||||
open my $ofh, ">", $out or fail("failed to render patched file");
|
||||
binmode $ofh;
|
||||
print $ofh $new or fail("failed to render patched file");
|
||||
close $ofh or fail("failed to render patched file");
|
||||
' "$@"
|
||||
}
|
||||
|
||||
replace_once() {
|
||||
target=$1
|
||||
search_file=$2
|
||||
replace_file=$3
|
||||
hunk_id=$4
|
||||
[ -e "$target" ] || die "file not found: $target"
|
||||
|
||||
fast_out=$(mk_temp)
|
||||
if replace_once_with_perl "$target" "$search_file" "$replace_file" "$hunk_id" "$allow_loose" "$fast_out"; then
|
||||
write_file "$target" "$fast_out"
|
||||
rm -f "$fast_out"
|
||||
return 0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
rm -f "$fast_out"
|
||||
[ "$status" = 127 ] || exit "$status"
|
||||
|
||||
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
|
||||
old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target"
|
||||
old=${old%"$marker"}
|
||||
search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search"
|
||||
search=${search%"$marker"}
|
||||
replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement"
|
||||
replacement=${replacement%"$marker"}
|
||||
|
||||
if [ -z "$search" ]; then
|
||||
[ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
|
||||
printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2
|
||||
new=$replacement$old
|
||||
else
|
||||
case "$old" in
|
||||
*"$search"*)
|
||||
prefix=${old%%"$search"*}
|
||||
suffix=${old#*"$search"}
|
||||
case "$suffix" in
|
||||
*"$search"*)
|
||||
[ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review"
|
||||
;;
|
||||
esac
|
||||
printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2
|
||||
new=$prefix$replacement$suffix
|
||||
;;
|
||||
*)
|
||||
die "hunk $hunk_id context not found in $target"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
out=$(mk_temp)
|
||||
printf '%s' "$new" > "$out" || die "failed to render patched file"
|
||||
write_file "$target" "$out"
|
||||
rm -f "$out"
|
||||
}
|
||||
|
||||
apply_update() {
|
||||
target=$1
|
||||
body=$2
|
||||
in_hunk=0
|
||||
search_file=
|
||||
replace_file=
|
||||
changed=0
|
||||
hunk_number=0
|
||||
has_add=0
|
||||
has_delete=0
|
||||
seen_add=0
|
||||
anchor_before_add=0
|
||||
anchor_after_add=0
|
||||
explicit_eof=0
|
||||
|
||||
finish_hunk() {
|
||||
[ "$in_hunk" = 1 ] || return 0
|
||||
if [ "$allow_loose" != 1 ]; then
|
||||
[ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
|
||||
if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then
|
||||
if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then
|
||||
die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
replace_once "$target" "$search_file" "$replace_file" "$hunk_number"
|
||||
rm -f "$search_file" "$replace_file"
|
||||
search_file=
|
||||
replace_file=
|
||||
in_hunk=0
|
||||
changed=1
|
||||
}
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
"*** End of File"*)
|
||||
continue
|
||||
;;
|
||||
"@@"*)
|
||||
finish_hunk
|
||||
search_file=$(mk_temp)
|
||||
replace_file=$(mk_temp)
|
||||
hunk_number=$((hunk_number + 1))
|
||||
has_add=0
|
||||
has_delete=0
|
||||
seen_add=0
|
||||
anchor_before_add=0
|
||||
anchor_after_add=0
|
||||
explicit_eof=0
|
||||
in_hunk=1
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
[ "$in_hunk" = 1 ] || die "expected hunk header in $target"
|
||||
case "$line" in
|
||||
" "*)
|
||||
text=${line#?}
|
||||
printf '%s\n' "$text" >> "$search_file"
|
||||
printf '%s\n' "$text" >> "$replace_file"
|
||||
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
|
||||
;;
|
||||
"-"*)
|
||||
text=${line#?}
|
||||
printf '%s\n' "$text" >> "$search_file"
|
||||
has_delete=1
|
||||
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
|
||||
;;
|
||||
"+"*)
|
||||
text=${line#?}
|
||||
printf '%s\n' "$text" >> "$replace_file"
|
||||
has_add=1
|
||||
seen_add=1
|
||||
;;
|
||||
"\\ No newline at end of file")
|
||||
;;
|
||||
"*** End of File"*)
|
||||
explicit_eof=1
|
||||
;;
|
||||
*)
|
||||
die "bad hunk line in $target: $line"
|
||||
;;
|
||||
esac
|
||||
done < "$body"
|
||||
|
||||
finish_hunk
|
||||
[ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target"
|
||||
}
|
||||
|
||||
is_top_header() {
|
||||
case "$1" in
|
||||
"*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
pushed=0
|
||||
pushed_line=
|
||||
next_patch_line() {
|
||||
if [ "$pushed" = 1 ]; then
|
||||
line=$pushed_line
|
||||
pushed=0
|
||||
pushed_line=
|
||||
return 0
|
||||
fi
|
||||
if IFS= read -r line; then
|
||||
return 0
|
||||
fi
|
||||
[ -n "${line:-}" ]
|
||||
}
|
||||
|
||||
push_patch_line() {
|
||||
pushed_line=$1
|
||||
pushed=1
|
||||
}
|
||||
|
||||
parse_add_file() {
|
||||
target=$1
|
||||
[ ! -e "$target" ] || die "file already exists: $target"
|
||||
tmp=$(mk_temp)
|
||||
while next_patch_line; do
|
||||
if is_top_header "$line"; then
|
||||
push_patch_line "$line"
|
||||
break
|
||||
fi
|
||||
case "$line" in
|
||||
"+"*)
|
||||
printf '%s\n' "${line#?}" >> "$tmp"
|
||||
;;
|
||||
*)
|
||||
rm -f "$tmp"
|
||||
die "add file lines must start with + for $target"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
write_file "$target" "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
parse_update_file() {
|
||||
target=$1
|
||||
move_to=
|
||||
body=$(mk_temp)
|
||||
if next_patch_line; then
|
||||
case "$line" in
|
||||
"*** Move to: "*)
|
||||
move_to=${line#"*** Move to: "}
|
||||
;;
|
||||
*)
|
||||
push_patch_line "$line"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
while next_patch_line; do
|
||||
if is_top_header "$line"; then
|
||||
push_patch_line "$line"
|
||||
break
|
||||
fi
|
||||
case "$line" in
|
||||
"*** Move to: "*)
|
||||
rm -f "$body"
|
||||
die "move marker must appear before update hunks"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$line" >> "$body"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -s "$body" ]; then
|
||||
apply_update "$target" "$body"
|
||||
elif [ -z "$move_to" ] && [ ! -e "$target" ]; then
|
||||
rm -f "$body"
|
||||
die "file not found: $target"
|
||||
fi
|
||||
rm -f "$body"
|
||||
if [ -n "$move_to" ]; then
|
||||
[ -e "$target" ] || die "file not found: $target"
|
||||
[ ! -e "$move_to" ] || die "target file already exists: $move_to"
|
||||
ensure_parent "$move_to"
|
||||
mv "$target" "$move_to" || die "failed to move $target to $move_to"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n'
|
||||
return 0
|
||||
;;
|
||||
--allow-loose)
|
||||
allow_loose=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
die "unsupported option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
next_patch_line || die "patch must start with *** Begin Patch"
|
||||
[ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch"
|
||||
while next_patch_line; do
|
||||
case "$line" in
|
||||
"*** End Patch")
|
||||
printf 'Done!\n'
|
||||
return 0
|
||||
;;
|
||||
"*** Add File: "*)
|
||||
parse_add_file "${line#"*** Add File: "}"
|
||||
;;
|
||||
"*** Delete File: "*)
|
||||
target=${line#"*** Delete File: "}
|
||||
rm -f "$target" || die "failed to delete $target"
|
||||
;;
|
||||
"*** Update File: "*)
|
||||
parse_update_file "${line#"*** Update File: "}"
|
||||
;;
|
||||
*)
|
||||
die "unexpected patch line: $line"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
die "missing *** End Patch"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="remote glob helper for UniDesk ssh passthrough")
|
||||
parser.add_argument("patterns", nargs="*", help="glob patterns relative to --root unless absolute")
|
||||
parser.add_argument("--root", default=".", help="base directory for relative patterns")
|
||||
parser.add_argument("--pattern", action="append", default=[], help="additional glob pattern")
|
||||
parser.add_argument("--contains", action="append", default=[], help="match path names containing text")
|
||||
parser.add_argument("--icontains", action="append", default=[], help="case-insensitive contains match")
|
||||
parser.add_argument("--type", choices=["any", "f", "d"], default="any", help="filter by any/file/dir")
|
||||
parser.add_argument("--limit", type=int, default=0, help="maximum number of rows to print")
|
||||
parser.add_argument("--sort", action="store_true", help="sort output")
|
||||
parser.add_argument("--absolute", action="store_true", help="print absolute paths")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.limit < 0:
|
||||
print("glob: --limit must be >= 0", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
root = os.path.abspath(args.root)
|
||||
patterns = list(args.patterns) + list(args.pattern)
|
||||
for text in args.contains:
|
||||
patterns.append(f"**/*{text}*")
|
||||
for text in args.icontains:
|
||||
# Python glob is case-sensitive on Linux, so filter from a broad recursive scan.
|
||||
patterns.append("**/*")
|
||||
if not patterns:
|
||||
patterns = ["*"]
|
||||
|
||||
seen = set()
|
||||
rows = []
|
||||
lowered_contains = [text.lower() for text in args.icontains]
|
||||
for pattern in patterns:
|
||||
effective = pattern if os.path.isabs(pattern) else os.path.join(root, pattern)
|
||||
for path in glob.iglob(effective, recursive=True):
|
||||
full = os.path.abspath(path)
|
||||
if full in seen:
|
||||
continue
|
||||
if lowered_contains and not any(text in os.path.basename(full).lower() or text in full.lower() for text in lowered_contains):
|
||||
continue
|
||||
if args.type == "f" and not os.path.isfile(full):
|
||||
continue
|
||||
if args.type == "d" and not os.path.isdir(full):
|
||||
continue
|
||||
seen.add(full)
|
||||
rows.append(full if args.absolute else os.path.relpath(full, root))
|
||||
|
||||
if args.sort:
|
||||
rows.sort()
|
||||
if args.limit > 0:
|
||||
rows = rows[:args.limit]
|
||||
for row in rows:
|
||||
print(row)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKIP_PARTS = {"node_modules", ".git", ".state", "logs", "references", "__pycache__"}
|
||||
|
||||
|
||||
def is_wsl():
|
||||
try:
|
||||
release = Path("/proc/sys/kernel/osrelease").read_text(errors="ignore").lower()
|
||||
except Exception:
|
||||
release = ""
|
||||
return "microsoft" in release or "wsl" in release or "WSL_INTEROP" in os.environ
|
||||
|
||||
|
||||
def to_windows_path(path):
|
||||
text = str(path)
|
||||
if text.startswith("/mnt/") and len(text) >= 7 and text[5].isalpha() and text[6] == "/":
|
||||
drive = text[5].upper()
|
||||
rest = text[7:].replace("/", "\\")
|
||||
return drive + ":\\" + rest
|
||||
return None
|
||||
|
||||
|
||||
def read_bounded(path, limit=16384):
|
||||
try:
|
||||
data = path.read_bytes()[:limit]
|
||||
return data.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def frontmatter_value(line):
|
||||
if ":" not in line:
|
||||
return None
|
||||
key, value = line.split(":", 1)
|
||||
return key.strip().lower(), value.strip().strip("\"'")
|
||||
|
||||
|
||||
def parse_skill_metadata(skill_md):
|
||||
text = read_bounded(skill_md)
|
||||
name = skill_md.parent.name
|
||||
description = ""
|
||||
lines = text.splitlines()
|
||||
if lines and lines[0].strip() == "---":
|
||||
for line in lines[1:]:
|
||||
if line.strip() == "---":
|
||||
break
|
||||
item = frontmatter_value(line)
|
||||
if item is None:
|
||||
continue
|
||||
key, value = item
|
||||
if key == "name" and value:
|
||||
name = value
|
||||
if key == "description" and value:
|
||||
description = value
|
||||
if not description:
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith("---") and not stripped.startswith("#"):
|
||||
description = stripped
|
||||
break
|
||||
return name, description
|
||||
|
||||
|
||||
def iter_skill_files(root, max_depth):
|
||||
try:
|
||||
iterator = root.rglob("SKILL.md")
|
||||
for skill_md in iterator:
|
||||
try:
|
||||
rel = skill_md.relative_to(root)
|
||||
except ValueError:
|
||||
continue
|
||||
directory_parts = rel.parts[:-1]
|
||||
if len(directory_parts) == 0 or len(directory_parts) > max_depth:
|
||||
continue
|
||||
if any(part in SKIP_PARTS for part in directory_parts):
|
||||
continue
|
||||
yield skill_md
|
||||
except Exception as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
|
||||
|
||||
def unique_paths(paths):
|
||||
seen = set()
|
||||
output = []
|
||||
for raw in paths:
|
||||
path = Path(raw).expanduser()
|
||||
key = str(path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
output.append(path)
|
||||
return output
|
||||
|
||||
|
||||
def default_wsl_roots():
|
||||
home = Path.home()
|
||||
roots = [home / ".agents" / "skills", home / ".codex" / "skills"]
|
||||
for raw in ("/root/.agents/skills", "/root/.codex/skills"):
|
||||
path = Path(raw)
|
||||
if str(path) not in {str(item) for item in roots}:
|
||||
roots.append(path)
|
||||
return roots
|
||||
|
||||
|
||||
def default_windows_roots():
|
||||
if not is_wsl():
|
||||
return []
|
||||
users = Path("/mnt/c/Users")
|
||||
roots = []
|
||||
try:
|
||||
children = list(users.iterdir()) if users.exists() else []
|
||||
except Exception:
|
||||
children = []
|
||||
for child in children:
|
||||
try:
|
||||
if not child.is_dir():
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
lower = child.name.lower()
|
||||
if lower in {"all users", "default", "default user", "public"}:
|
||||
continue
|
||||
roots.append(child / ".agents" / "skills")
|
||||
roots.append(child / ".codex" / "skills")
|
||||
return roots
|
||||
|
||||
|
||||
def scan_root(scope, root, max_depth):
|
||||
try:
|
||||
root_exists = root.exists()
|
||||
root_error = None
|
||||
except Exception as exc:
|
||||
root_exists = False
|
||||
root_error = str(exc)
|
||||
record = {
|
||||
"scope": scope,
|
||||
"path": str(root),
|
||||
"windowsPath": to_windows_path(root),
|
||||
"exists": root_exists,
|
||||
"skillCount": 0,
|
||||
"error": root_error,
|
||||
}
|
||||
skills = []
|
||||
if not record["exists"]:
|
||||
return record, skills
|
||||
try:
|
||||
for skill_md in iter_skill_files(root, max_depth):
|
||||
name, description = parse_skill_metadata(skill_md)
|
||||
skill = {
|
||||
"scope": scope,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"path": str(skill_md.parent),
|
||||
"skillMd": str(skill_md),
|
||||
"windowsPath": to_windows_path(skill_md.parent),
|
||||
"root": str(root),
|
||||
}
|
||||
skills.append(skill)
|
||||
except Exception as exc:
|
||||
record["error"] = str(exc)
|
||||
record["skillCount"] = len(skills)
|
||||
return record, skills
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="discover WSL/Linux and Windows skill directories from a UniDesk ssh passthrough session")
|
||||
parser.add_argument("--scope", choices=["all", "wsl", "windows"], default="all", help="which skill roots to scan")
|
||||
parser.add_argument("--max-depth", type=int, default=4, help="maximum directory depth below each skill root")
|
||||
parser.add_argument("--limit", type=int, default=0, help="maximum skill rows to return; 0 means unlimited")
|
||||
parser.add_argument("--root", action="append", default=[], help="extra WSL/Linux skill root")
|
||||
parser.add_argument("--windows-root", action="append", default=[], help="extra Windows skill root, expressed as /mnt/<drive>/...")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.max_depth <= 0:
|
||||
print(json.dumps({"ok": False, "error": "--max-depth must be positive"}, ensure_ascii=False))
|
||||
return 2
|
||||
if args.limit < 0:
|
||||
print(json.dumps({"ok": False, "error": "--limit must be >= 0"}, ensure_ascii=False))
|
||||
return 2
|
||||
|
||||
roots = []
|
||||
if args.scope in ("all", "wsl"):
|
||||
roots.extend(("wsl", path) for path in default_wsl_roots())
|
||||
roots.extend(("wsl", Path(raw).expanduser()) for raw in args.root)
|
||||
if args.scope in ("all", "windows"):
|
||||
roots.extend(("windows", path) for path in default_windows_roots())
|
||||
roots.extend(("windows", Path(raw).expanduser()) for raw in args.windows_root)
|
||||
|
||||
seen = set()
|
||||
unique = []
|
||||
for scope, path in roots:
|
||||
key = (scope, str(path))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append((scope, path))
|
||||
|
||||
root_records = []
|
||||
skills = []
|
||||
for scope, root in unique:
|
||||
record, found = scan_root(scope, root, args.max_depth)
|
||||
root_records.append(record)
|
||||
skills.extend(found)
|
||||
|
||||
scope_order = {"wsl": 0, "windows": 1}
|
||||
skills.sort(key=lambda item: (scope_order.get(str(item["scope"]), 9), str(item["name"]).lower(), str(item["path"])))
|
||||
total_before_limit = len(skills)
|
||||
if args.limit > 0:
|
||||
skills = skills[:args.limit]
|
||||
|
||||
counts = {"total": len(skills), "totalBeforeLimit": total_before_limit, "wsl": 0, "windows": 0}
|
||||
for skill in skills:
|
||||
scope = str(skill["scope"])
|
||||
if scope in counts:
|
||||
counts[scope] += 1
|
||||
|
||||
payload = {
|
||||
"ok": True,
|
||||
"command": "unidesk ssh skills",
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"node": {
|
||||
"hostname": socket.gethostname(),
|
||||
"user": getpass.getuser(),
|
||||
"home": str(Path.home()),
|
||||
"platform": platform.platform(),
|
||||
"isWsl": is_wsl(),
|
||||
"python": sys.version.split()[0],
|
||||
},
|
||||
"counts": counts,
|
||||
"roots": root_records,
|
||||
"skills": skills,
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user