# Script Completions Guide This guide explains how to add intelligent shell completions to your scripts in the remote script loader system. ## Overview Scripts can export a `getCompletions` function that provides context-aware suggestions for command-line arguments. The loader daemon calls this function when users press TAB in their shell. ## Quick Start Add this to your script: ```typescript import type { CompletionContext } from "./completions.d.ts"; export function getCompletions(ctx: CompletionContext): string[] { return ["--help", "--version", "--verbose"]; } ``` Or use async for API calls or async operations: ```typescript import type { CompletionContext } from "./completions.d.ts"; export async function getCompletions(ctx: CompletionContext): Promise { // Can now use await for async operations return ["--help", "--version", "--verbose"]; } ``` That's it! The loader will automatically discover and use your completion function. ## The CompletionContext Your function receives a context object with three properties: ```typescript interface CompletionContext { args: string[]; // All arguments provided so far current: string; // The word being completed position: number; // Position in the args array } ``` ### Example Context If the user types: ```bash scripts my-script --flag1 value1 --fl ``` Your function receives: ```typescript { args: ["--flag1", "value1"], current: "--fl", position: 2 } ``` ## Common Patterns ### 1. Basic Flag Completions Return all available flags: ```typescript export function getCompletions(ctx: CompletionContext): string[] { return [ "--input=", "--output=", "--verbose", "--help" ]; } ``` ### 2. Value Completions Suggest values for flags with `=`: ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { current } = ctx; if (current.startsWith("--format=")) { return ["--format=json", "--format=yaml", "--format=xml"]; } if (current.startsWith("--level=")) { return ["--level=debug", "--level=info", "--level=warn", "--level=error"]; } return ["--format=", "--level=", "--help"]; } ``` ### 3. Avoiding Duplicate Flags Filter out already-used flags: ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { args } = ctx; // Check what's already provided const hasVerbose = args.includes("--verbose"); const hasOutput = args.some(a => a.startsWith("--output")); const flags = []; if (!hasVerbose) flags.push("--verbose"); if (!hasOutput) flags.push("--output="); flags.push("--help"); // Help can always be shown return flags; } ``` ### 4. Mutually Exclusive Options ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { args } = ctx; const hasJson = args.includes("--json"); const hasYaml = args.includes("--yaml"); // Don't suggest both formats at once if (hasJson) { return ["--verbose", "--help"]; // No --yaml } if (hasYaml) { return ["--verbose", "--help"]; // No --json } return ["--json", "--yaml", "--verbose", "--help"]; } ``` ### 5. Filesystem Completions Discover files dynamically: ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { current } = ctx; if (current.startsWith("--config=")) { try { const home = Deno.env.get("HOME") || ""; const configDir = `${home}/.config/myapp`; const configs: string[] = []; for (const entry of Deno.readDirSync(configDir)) { if (entry.isFile && entry.name.endsWith(".json")) { configs.push(`--config=${entry.name}`); } } return configs.length > 0 ? configs : ["--config=config.json"]; } catch { return ["--config=config.json"]; } } return ["--config=", "--help"]; } ``` ### 6. Environment-Based Completions ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { current } = ctx; if (current.startsWith("--user=")) { // Suggest current user const currentUser = Deno.env.get("USER") || "user"; return [`--user=${currentUser}`]; } if (current.startsWith("--home=")) { const home = Deno.env.get("HOME") || ""; return [`--home=${home}`]; } return ["--user=", "--home=", "--help"]; } ``` ### 7. Async Completions with API Calls ```typescript export async function getCompletions(ctx: CompletionContext): Promise { const { current } = ctx; if (current.startsWith("--repo=")) { try { // Fetch repositories from GitHub API const response = await fetch("https://api.github.com/user/repos", { headers: { "Authorization": `token ${Deno.env.get("GITHUB_TOKEN")}` } }); if (response.ok) { const repos = await response.json(); return repos.map((r: { name: string }) => `--repo=${r.name}`); } } catch { // Fallback on error } return ["--repo="]; } return ["--repo=", "--help"]; } ``` **Note**: The loader daemon caches script modules, but your async operations run on each completion. For better performance, consider returning fast fallback values while keeping the function synchronous, or implement your own caching within the script. ### 8. Positional Argument Completions ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { current, args, position } = ctx; // Get non-flag arguments const positionalArgs = args.filter(a => !a.startsWith("-")); // First positional: suggest commands if (positionalArgs.length === 0 && !current.startsWith("-")) { return ["start", "stop", "restart", "status"]; } // Second positional: suggest targets if (positionalArgs.length === 1 && !current.startsWith("-")) { return ["production", "staging", "development"]; } // Otherwise suggest flags return ["--force", "--verbose", "--help"]; } ``` ## Best Practices ### Performance - Keep completion functions fast (< 50ms recommended) - Cache expensive operations if possible - **Use async sparingly**: Async completions can make UX slower - For API calls, implement caching or provide immediate fallback - Use try-catch for filesystem operations ```typescript export function getCompletions(ctx: CompletionContext): string[] { try { // Fast filesystem lookup const files = [...Deno.readDirSync(".")]; return files.map(f => f.name); } catch { // Fallback to safe defaults return ["file.txt", "config.json"]; } } ``` ### User Experience - Return empty array if no completions available - Include both complete and partial suggestions - Add `=` suffix for flags that need values - Provide sensible defaults when dynamic lookup fails ```typescript export function getCompletions(ctx: CompletionContext): string[] { const { current } = ctx; // Partial match - suggests user needs to provide a value if (current.startsWith("--port=")) { return ["--port=3000", "--port=8080", "--port=8443"]; } // Complete flags return ["--port=", "--host=", "--help"]; } ``` ### Documentation - Add JSDoc comments to your completion function - Document expected flag values - Explain any complex completion logic ```typescript /** * Provides completions for the configure script. * * Supports: * - --key=: SSH key paths from ~/.ssh/ * - --port=: Common SSH ports (22, 2222, 2200) * - --alias=: Auto-generated from target */ export function getCompletions(ctx: CompletionContext): string[] { // ... implementation } ``` ## Testing Completions Locally Test your completion function locally before pushing: ```bash # Import and test directly deno eval ' const m = await import("file:///path/to/your/script.ts"); const result = m.getCompletions({ args: ["--flag1"], current: "--", position: 1 }); console.log(result.join("\n")); ' ``` ## Caching and Performance The loader daemon caches: - **Script modules**: 5 minutes (respects HTTP Cache-Control) - **HTTP responses**: Uses ETag/Last-Modified for conditional requests - **Metadata (refs, script lists)**: 60 seconds Your completion function is called on every TAB press, but the script import is cached, so only your function execution time matters. ## Advanced Example Complete example from `configure_ssh_key.ts`: ```typescript import type { CompletionContext } from "./completions.d.ts"; export function getCompletions(ctx: CompletionContext): string[] { const { current, args } = ctx; // Complete SSH key paths if (current.startsWith("--key=")) { try { const home = Deno.env.get("HOME") || ""; const sshDir = `${home}/.ssh`; const keys: string[] = []; for (const entry of Deno.readDirSync(sshDir)) { if (entry.isFile && !entry.name.endsWith(".pub") && (entry.name.startsWith("id_") || entry.name.includes("key"))) { keys.push(`--key=~/.ssh/${entry.name}`); } } return keys.length > 0 ? keys : ["--key=~/.ssh/id_rsa"]; } catch { return ["--key=~/.ssh/id_rsa", "--key=~/.ssh/id_ed25519"]; } } // Smart alias based on target if (current.startsWith("--alias=")) { 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="]; } // Common ports if (current.startsWith("--port=")) { return ["--port=22", "--port=2222", "--port=2200"]; } // Track what's been used const hasGenerate = args.includes("--generate"); const hasKey = args.some(a => a.startsWith("--key")); const flags: string[] = []; // Mutually exclusive: --key and --generate if (!hasGenerate && !hasKey) { flags.push("--key=", "--generate"); } flags.push("--alias=", "--port=", "--force", "--dry-run", "--help"); return flags; } ``` ## Troubleshooting ### Completions not working? 1. Check script exports `getCompletions`: ```typescript export function getCompletions(ctx: CompletionContext): string[] { ... } ``` 2. Verify the function signature matches: ```typescript (ctx: { args: string[]; current: string; position: number }) => string[] ``` 3. Test locally with the eval command above 4. Check daemon logs: `cat /tmp/daemon.log` ### Completions are slow? - Profile your function execution time - Avoid expensive I/O operations - Cache results when possible - Return early for invalid states ### Completions are incorrect? - Log the context to understand what's being passed: ```typescript export function getCompletions(ctx: CompletionContext): string[] { console.error("DEBUG:", JSON.stringify(ctx)); return [...]; } ``` - Check daemon logs for your debug output ## Type Checking Use the provided type definitions: ```typescript import type { CompletionContext, CompletionFunction } from "./completions.d.ts"; export const getCompletions: CompletionFunction = (ctx) => { // TypeScript will enforce correct parameter and return types return ["--help"]; }; ```