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
11 KiB
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:
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:
import type { CompletionContext } from "./completions.d.ts";
export async function getCompletions(ctx: CompletionContext): Promise<string[]> {
// 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:
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:
scripts my-script --flag1 value1 --fl<TAB>
Your function receives:
{
args: ["--flag1", "value1"],
current: "--fl",
position: 2
}
Common Patterns
1. Basic Flag Completions
Return all available flags:
export function getCompletions(ctx: CompletionContext): string[] {
return [
"--input=",
"--output=",
"--verbose",
"--help"
];
}
2. Value Completions
Suggest values for flags with =:
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:
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
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:
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
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
export async function getCompletions(ctx: CompletionContext): Promise<string[]> {
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=<repository-name>"];
}
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
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
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
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
/**
* Provides completions for the configure script.
*
* Supports:
* - --key=<path>: SSH key paths from ~/.ssh/
* - --port=<num>: Common SSH ports (22, 2222, 2200)
* - --alias=<name>: Auto-generated from target
*/
export function getCompletions(ctx: CompletionContext): string[] {
// ... implementation
}
Testing Completions Locally
Test your completion function locally before pushing:
# 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:
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=<host-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?
-
Check script exports
getCompletions:export function getCompletions(ctx: CompletionContext): string[] { ... } -
Verify the function signature matches:
(ctx: { args: string[]; current: string; position: number }) => string[] -
Test locally with the eval command above
-
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:
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:
import type { CompletionContext, CompletionFunction } from "./completions.d.ts";
export const getCompletions: CompletionFunction = (ctx) => {
// TypeScript will enforce correct parameter and return types
return ["--help"];
};