@xynogen/pix-ssh 0.2.1 → 0.2.3
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 +6 -3
- package/package.json +1 -1
- package/src/index.ts +22 -11
- package/src/lib.test.ts +15 -0
- package/src/lib.ts +33 -0
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# pix-ssh
|
|
2
2
|
|
|
3
|
-
Pi tool — `ssh_run`: run a
|
|
3
|
+
Pi tool — `ssh_run`: run a command through a remote host's configured SSH shell, optionally through POSIX `sudo`.
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> Basic Windows `cmd`, Windows PowerShell, and `pwsh` commands may work through the configured SSH shell, but support is best-effort. Shell selection, complex quoting, PowerShell error/stream/encoding semantics, interactive prompts, and Windows administrator/UAC elevation are not covered. The agent should back away when correctness depends on those limits. `sudo: true` supports POSIX `sudo` only.
|
|
4
7
|
|
|
5
8
|
## What it does
|
|
6
9
|
|
|
7
|
-
Registers the `ssh_run` tool, which executes a command on a remote machine behind a permission dialog (the shared overlay from `@xynogen/pix-pretty`, the same one pix-sudo uses).
|
|
10
|
+
Registers the `ssh_run` tool, which executes a command through the configured SSH shell on a remote machine behind a permission dialog (the shared overlay from `@xynogen/pix-pretty`, the same one pix-sudo uses). Initial and privileged calls require Allow/Deny approval in the UI, with a 60-second auto-deny timeout. Non-privileged calls may be auto-approved during the 15-minute per-host approval window or in YOLO mode when no password is missing; each host-window auto-approval emits a notification. Output is truncated to 50 KB / 2000 lines. In non-interactive (RPC/JSON) mode the tool is blocked immediately.
|
|
8
11
|
|
|
9
|
-
**Parameters:** `host` as `[user@]host[:port]` (e.g. `deploy@10.0.0.5:2222`), `command`, optional `sudo` (run
|
|
12
|
+
**Parameters:** `host` as `[user@]host[:port]` (e.g. `deploy@10.0.0.5:2222`), `command`, optional `sudo` (run through POSIX `sudo` as root), optional `reason`.
|
|
10
13
|
|
|
11
14
|
### Authentication
|
|
12
15
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
MAX_OUTPUT_LINES,
|
|
60
60
|
parseHost,
|
|
61
61
|
probeKeyAuth,
|
|
62
|
+
resolveSshHost,
|
|
62
63
|
runSsh,
|
|
63
64
|
truncate,
|
|
64
65
|
} from "./lib.ts";
|
|
@@ -216,17 +217,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
216
217
|
name: "ssh_run",
|
|
217
218
|
label: "Run over SSH",
|
|
218
219
|
description:
|
|
219
|
-
"Run a
|
|
220
|
-
"
|
|
221
|
-
"
|
|
220
|
+
"Run a command through the remote host's configured SSH shell, optionally through POSIX sudo. " +
|
|
221
|
+
"Basic cmd/PowerShell/pwsh commands may work, but Windows shells are best-effort: shell selection, " +
|
|
222
|
+
"quoting, PowerShell error/stream/encoding semantics, interactive prompts, and Windows " +
|
|
223
|
+
"administrator/UAC elevation are not supported. Back away and tell the user when correctness " +
|
|
224
|
+
"depends on one of those limits. Handles connection and password entry through a permission dialog. " +
|
|
225
|
+
"A configured approval window or YOLO mode may auto-approve non-privileged commands when no password is missing. " +
|
|
222
226
|
"SSH auth tries key/agent first, then prompts for a login password if needed. " +
|
|
223
227
|
"Set `sudo: true` to run the command as root on the remote machine (prompts for the " +
|
|
224
228
|
"remote sudo password). Always provide a clear `reason`.",
|
|
225
|
-
promptSnippet: "Run a
|
|
229
|
+
promptSnippet: "Run a remote SSH command (Windows shells best-effort; POSIX sudo only)",
|
|
226
230
|
promptGuidelines: [
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
231
|
+
"ssh_run sends commands to the remote host's configured SSH shell. Basic cmd, PowerShell, or " +
|
|
232
|
+
"pwsh commands may work, but treat Windows shells as best-effort. Back away and tell the user " +
|
|
233
|
+
"when correctness depends on explicit shell selection, complex quoting, PowerShell error/stream/encoding " +
|
|
234
|
+
"semantics, interactive prompts, or Windows administrator/UAC elevation. `sudo` covers POSIX sudo " +
|
|
235
|
+
"only. Provide `host` as `[user@]host[:port]` and always explain the intent in `reason`.",
|
|
230
236
|
],
|
|
231
237
|
|
|
232
238
|
renderShell: "self",
|
|
@@ -236,7 +242,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
236
242
|
description: "Remote target as `[user@]host[:port]` (e.g. `deploy@10.0.0.5:2222`).",
|
|
237
243
|
}),
|
|
238
244
|
command: Type.String({
|
|
239
|
-
description: "
|
|
245
|
+
description: "Command sent to the remote host's configured SSH shell.",
|
|
240
246
|
}),
|
|
241
247
|
sudo: Type.Optional(
|
|
242
248
|
Type.Boolean({
|
|
@@ -268,9 +274,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
268
274
|
isError: true,
|
|
269
275
|
};
|
|
270
276
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
277
|
+
// ponytail: let OpenSSH own config parsing; `ssh -G` handles aliases,
|
|
278
|
+
// Include, and Match rules without duplicating its config grammar.
|
|
279
|
+
const effectiveSpec = await resolveSshHost(spec, sig);
|
|
280
|
+
const host = hostTarget(effectiveSpec);
|
|
281
|
+
// Keep original target for execution so alias-specific IdentityFile,
|
|
282
|
+
// ProxyJump, and other SSH config options still apply.
|
|
283
|
+
const controlPath = controlPathFor(effectiveSpec);
|
|
284
|
+
const key = cacheKey(effectiveSpec);
|
|
274
285
|
const creds = credCache.get(key) ?? {};
|
|
275
286
|
|
|
276
287
|
const g = globalThis as { __pixAfk?: boolean; __pixYolo?: boolean };
|
package/src/lib.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
hostTarget,
|
|
11
11
|
isUnreachable,
|
|
12
12
|
parseHost,
|
|
13
|
+
parseSshConfig,
|
|
13
14
|
remoteCommand,
|
|
14
15
|
shellQuote,
|
|
15
16
|
truncate,
|
|
@@ -46,6 +47,20 @@ describe("parseHost", () => {
|
|
|
46
47
|
});
|
|
47
48
|
});
|
|
48
49
|
|
|
50
|
+
describe("parseSshConfig", () => {
|
|
51
|
+
it("reads effective user, hostname, and port from ssh -G output", () => {
|
|
52
|
+
expect(parseSshConfig("host orin\nuser jetson\nhostname 10.10.21.251\nport 2222\n")).toEqual({
|
|
53
|
+
user: "jetson",
|
|
54
|
+
host: "10.10.21.251",
|
|
55
|
+
port: 2222,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects incomplete ssh -G output", () => {
|
|
60
|
+
expect(parseSshConfig("hostname 10.10.21.251\n")).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
49
64
|
describe("hostTarget", () => {
|
|
50
65
|
it("joins user and host", () => {
|
|
51
66
|
expect(hostTarget({ user: "a", host: "b" })).toBe("a@b");
|
package/src/lib.ts
CHANGED
|
@@ -108,6 +108,39 @@ function parsePort(value: string): number {
|
|
|
108
108
|
return n;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Parse effective destination fields emitted by OpenSSH's `ssh -G`. */
|
|
112
|
+
export function parseSshConfig(output: string): HostSpec | undefined {
|
|
113
|
+
const values = new Map(
|
|
114
|
+
output
|
|
115
|
+
.split("\n")
|
|
116
|
+
.map((line) => line.trim().split(/\s+/, 2))
|
|
117
|
+
.filter((parts): parts is [string, string] => parts.length === 2),
|
|
118
|
+
);
|
|
119
|
+
const user = values.get("user");
|
|
120
|
+
const host = values.get("hostname");
|
|
121
|
+
const port = values.get("port");
|
|
122
|
+
if (!user || !host || !port) return undefined;
|
|
123
|
+
return { user, host, port: parsePort(port) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve aliases through OpenSSH config, including Include and Match rules. */
|
|
127
|
+
export function resolveSshHost(spec: HostSpec, signal?: AbortSignal): Promise<HostSpec> {
|
|
128
|
+
const args = ["-G"];
|
|
129
|
+
if (spec.port !== undefined) args.push("-p", String(spec.port));
|
|
130
|
+
args.push(hostTarget(spec));
|
|
131
|
+
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
let stdout = "";
|
|
134
|
+
const proc = spawn("ssh", args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
135
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
136
|
+
stdout += chunk.toString();
|
|
137
|
+
});
|
|
138
|
+
proc.on("error", () => resolve(spec));
|
|
139
|
+
proc.on("close", (code) => resolve(code === 0 ? (parseSshConfig(stdout) ?? spec) : spec));
|
|
140
|
+
signal?.addEventListener("abort", () => proc.kill("SIGTERM"), { once: true });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
/** Canonical `[user@]host` target string for ssh argv. */
|
|
112
145
|
export function hostTarget(spec: HostSpec): string {
|
|
113
146
|
return spec.user ? `${spec.user}@${spec.host}` : spec.host;
|