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:
parent
616a2d9c96
commit
df5df0ea92
7 changed files with 1448 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue