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

447
COMPLETIONS.md Normal file
View file

@ -0,0 +1,447 @@
# 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"];
};
```