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
447 lines
11 KiB
Markdown
447 lines
11 KiB
Markdown
# 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<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:
|
|
|
|
```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<TAB>
|
|
```
|
|
|
|
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<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
|
|
|
|
```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=<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:
|
|
|
|
```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=<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?
|
|
|
|
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"];
|
|
};
|
|
```
|