Add remote script loader with intelligent completions

Features:
- Remote script execution with version control (name@ref)
- Background daemon for fast completions (< 15ms)
- HTTP caching with ETag/Last-Modified support
- Context-aware completions with async support
- Comprehensive documentation and type definitions

Changes:
- Add loader.ts with daemon mode
- Add completions.d.ts type definitions
- Add COMPLETIONS.md guide for developers
- Add README.md with usage examples
- Update configure_ssh_key.ts with async completions
- Use JSR imports for remote execution compatibility
This commit is contained in:
Karolis2011 2025-11-29 23:22:35 +02:00
parent 616a2d9c96
commit df5df0ea92
7 changed files with 1448 additions and 3 deletions

View file

@ -26,9 +26,117 @@
* from available keys in ~/.ssh or ssh-agent.
*/
import { parseArgs } from "@std/cli/parse-args";
import { expandGlob } from "@std/fs/expand-glob";
import { join } from "@std/path";
// Export completion function for loader
import type { CompletionContext } from "./completions.d.ts";
export async function getCompletions(ctx: CompletionContext): Promise<string[]> {
const { current, args } = ctx;
// Helper to find SSH keys in ~/.ssh/
const findSSHKeys = async (): Promise<string[]> => {
try {
const home = Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || "";
if (!home) return [];
const sshDir = `${home}/.ssh`;
const keys: string[] = [];
try {
for await (const entry of Deno.readDir(sshDir)) {
if (entry.isFile && !entry.name.endsWith(".pub") &&
(entry.name.startsWith("id_") || entry.name.includes("key"))) {
keys.push(`--key=~/.ssh/${entry.name}`);
}
}
} catch {
// Fallback to common names if can't read directory
return ["--key=~/.ssh/id_rsa", "--key=~/.ssh/id_ed25519", "--key=~/.ssh/id_ecdsa"];
}
return keys.length > 0 ? keys : ["--key=~/.ssh/id_rsa", "--key=~/.ssh/id_ed25519"];
} catch {
return ["--key=~/.ssh/id_rsa", "--key=~/.ssh/id_ed25519"];
}
};
// If completing a value after --key=
if (current.startsWith("--key=")) {
const prefix = "--key=";
const value = current.slice(prefix.length);
// If value is empty or starts with ~, suggest keys
if (!value || value.startsWith("~")) {
return await findSSHKeys();
}
// Otherwise suggest path completion hint
return ["--key=<path-to-private-key>"];
}
// If completing --alias= value
if (current.startsWith("--alias=")) {
// Suggest format based on first positional arg if present
const target = args.find(a => !a.startsWith("-"));
if (target && target.includes("@")) {
const [user, host] = target.split("@");
const hostPart = host.split(":")[0];
return [`--alias=${user}-${hostPart.replace(/\./g, "-")}`];
}
return ["--alias=<host-alias>"];
}
// If completing --port= value
if (current.startsWith("--port=")) {
return ["--port=22", "--port=2222", "--port=2200"];
}
// If no flags yet and no target, suggest target format
const hasTarget = args.some(a => !a.startsWith("-") && a.includes("@"));
if (!hasTarget && !current.startsWith("-")) {
return ["user@hostname", "user@hostname:port"];
}
// Check if certain flags already provided to avoid duplicates
const hasGenerate = args.includes("--generate");
const hasKey = args.some(a => a.startsWith("--key"));
const hasAlias = args.some(a => a.startsWith("--alias"));
const hasPort = args.some(a => a.startsWith("--port"));
const hasForce = args.includes("--force");
const hasDryRun = args.includes("--dry-run");
const options: string[] = [];
// Only suggest --key if --generate not present
if (!hasGenerate && !hasKey) {
options.push("--key=");
}
if (!hasGenerate && !hasKey) {
options.push("--generate");
}
if (!hasAlias) {
options.push("--alias=");
}
if (!hasPort) {
options.push("--port=");
}
if (!hasForce) {
options.push("--force");
}
if (!hasDryRun) {
options.push("--dry-run");
}
options.push("--help");
return options;
}
import { parseArgs } from "jsr:@std/cli@^1.0.0/parse-args";
import { expandGlob } from "jsr:@std/fs@^1.0.0/expand-glob";
import { join } from "jsr:@std/path@^1.0.0";
interface ParsedTarget {
user: string;