Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,6 @@ List forks of a bucket.
forks my-bucket
```

### bundle

Batch-download multiple files as a tar archive.

```bash
bundle file1.txt file2.txt # Download as tar
bundle file1.txt file2.txt --gzip # Download as gzip tar
bundle file1.txt file2.txt --zstd # Download as zstd tar
```

## Multi-Bucket

Mount multiple buckets at different paths:
Expand Down Expand Up @@ -289,7 +279,6 @@ new TigrisShell(config: TigrisConfig, shellOptions?: ShellOptions)
| `createSnapshotCommand(config)` | Create snapshot command only |
| `createForkCommand(config)` | Create fork command only |
| `createForksListCommand(config)` | Create forks command only |
| `createBundleCommand(config)` | Create bundle command only |

## Examples

Expand Down
13 changes: 9 additions & 4 deletions playground/src/shell-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { ReplSession } from "@tigrisdata/agent-shell/repl";
import type { Terminal } from "@xterm/xterm";
import { browserLogin } from "./auth.js";

const PROMPT = "\x1b[32m$ \x1b[0m";
const GREEN = "\x1b[32m";
const RESET = "\x1b[0m";

/**
* Thin xterm.js adapter over ReplSession.
Expand All @@ -26,6 +27,10 @@ export class ShellLoop {
this.session = new ReplSession({ loginFn: browserLogin });
}

private get promptStr(): string {
return `${GREEN}${this.session.promptText}${RESET}`;
}

start() {
this.prompt();
this.terminal.onData((data) => this.handleInput(data));
Expand All @@ -49,7 +54,7 @@ export class ShellLoop {
}

private prompt() {
this.terminal.write(`\r\n${PROMPT}`);
this.terminal.write(`\r\n${this.promptStr}`);
this.currentLine = "";
this.cursorPos = 0;
}
Expand Down Expand Up @@ -116,7 +121,7 @@ export class ShellLoop {

if (data === "\x0c") {
this.terminal.clear();
const prefix = this.pendingPromptResolve ? this.pendingPromptText : PROMPT;
const prefix = this.pendingPromptResolve ? this.pendingPromptText : this.promptStr;
this.terminal.write(prefix + this.currentLine);
return;
}
Expand Down Expand Up @@ -170,7 +175,7 @@ export class ShellLoop {
}

private redrawLine() {
const prefix = this.pendingPromptResolve ? this.pendingPromptText : PROMPT;
const prefix = this.pendingPromptResolve ? this.pendingPromptText : this.promptStr;
this.terminal.write(`\r\x1b[K${prefix}${this.currentLine}`);
const back = this.currentLine.length - this.cursorPos;
if (back > 0) {
Expand Down
20 changes: 20 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,28 @@ async function main() {
const args = parseArgs(rawArgs);
const session = new ReplSession({ loginFn: deviceLogin });

const REPL_COMMANDS = [
"login",
"configure",
"mount",
"umount",
"df",
"flush",
"whoami",
"logout",
"clear",
"help",
"exit",
];

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "$ ",
completer: (line: string) => {
const hits = REPL_COMMANDS.filter((cmd) => cmd.startsWith(line));
return [hits.length ? hits : REPL_COMMANDS, line];
},
});

const io: ReplIO = {
Expand Down Expand Up @@ -110,6 +128,7 @@ async function main() {
io.write("Type 'help' for available commands.\n\n");

// Process lines sequentially using async iterator
rl.setPrompt(session.promptText);
rl.prompt();

for await (const line of rl) {
Expand All @@ -125,6 +144,7 @@ async function main() {
await session.handle(trimmed, io);
}

rl.setPrompt(session.promptText);
rl.prompt();
}

Expand Down
65 changes: 0 additions & 65 deletions src/commands/bundle.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Command } from "just-bash";
import type { TigrisConfig } from "../types.js";
import { createBundleCommand } from "./bundle.js";
import { createForkCommand, createForksListCommand } from "./fork.js";
import { createPresignCommand } from "./presign.js";
import { createSnapshotCommand } from "./snapshot.js";
Expand All @@ -14,11 +13,9 @@ export function createTigrisCommands(config: TigrisConfig): Command[] {
createSnapshotCommand(config),
createForkCommand(config),
createForksListCommand(config),
createBundleCommand(config),
];
}

export { createBundleCommand } from "./bundle.js";
export { createForkCommand, createForksListCommand } from "./fork.js";
export { createPresignCommand } from "./presign.js";
export { createSnapshotCommand } from "./snapshot.js";
8 changes: 8 additions & 0 deletions src/commands/presign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export function createPresignCommand(config: TigrisConfig) {
};
}

if (!config.accessKeyId) {
return {
stdout: "",
stderr: "presign: requires access key auth. Use 'configure' instead of 'login'.\n",
exitCode: 1,
};
}

const { expiresIn, operation } = parsePresignArgs(args.slice(1));
const key = path.startsWith("/") ? path.slice(1) : path;
const result = await getPresignedUrl(key, {
Expand Down
26 changes: 18 additions & 8 deletions src/repl/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,32 +108,32 @@ export class ReplSession {
const mountPoint = `/${options.bucket}`;
const newShell = new TigrisShell(newConfig, { cwd: mountPoint });
this.commitSession(newConfig, newShell, "access-key");
this.cwd = mountPoint;
io.write(`Configured. Mounted ${options.bucket} at ${mountPoint}\n`);
} else {
const newShell = new TigrisShell(newConfig);
await this.listAndMountBuckets(newConfig, newShell, "access-key", io);
await this.listAndMountBuckets(newConfig, "access-key", io);
}
}

/** Shared: list buckets, auto-mount first, commit session. */
private async listAndMountBuckets(
newConfig: TigrisConfig,
newShell: TigrisShell,
authMethod: "access-key" | "oauth",
io: ReplIO,
): Promise<void> {
const bucketsResult = await listBuckets({ config: newConfig });
if ("error" in bucketsResult) {
const newShell = new TigrisShell(newConfig);
this.commitSession(newConfig, newShell, authMethod);
io.write(`Could not list buckets: ${bucketsResult.error.message}\n`);
io.write("Use 'mount <bucket> <path>' to mount manually.\n");
return;
}

this.commitSession(newConfig, newShell, authMethod);

const bucketNames = bucketsResult.data.buckets.map((b) => b.name);
if (bucketNames.length === 0) {
const newShell = new TigrisShell(newConfig);
this.commitSession(newConfig, newShell, authMethod);
io.write("No buckets found.\n");
io.write("Use 'mount <bucket> <path>' to mount manually.\n");
return;
Expand All @@ -146,8 +146,11 @@ export class ReplSession {

const first = bucketNames[0];
if (first) {
// Create shell with the first bucket so commands get the right config
newConfig.bucket = first;
const mountPoint = `/${first}`;
this.shell?.mount(first, mountPoint);
const newShell = new TigrisShell(newConfig, { cwd: mountPoint });
this.commitSession(newConfig, newShell, authMethod);
Comment thread
designcode marked this conversation as resolved.
this.cwd = mountPoint;
io.write(`\nMounted ${first} at ${mountPoint}\n`);
}
Expand Down Expand Up @@ -214,9 +217,8 @@ export class ReplSession {
organizationId: selectedOrg.id,
};

const newShell = new TigrisShell(newConfig);
this.email = result.email;
await this.listAndMountBuckets(newConfig, newShell, "oauth", io);
await this.listAndMountBuckets(newConfig, "oauth", io);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
io.write(`login: ${message}\n`);
Expand Down Expand Up @@ -428,4 +430,12 @@ export class ReplSession {
get isConfigured(): boolean {
return this.shell !== null;
}

/** Get the current prompt string (e.g. "/my-bucket $ "). */
get promptText(): string {
if (this.cwd) {
return `${this.cwd} $ `;
}
return "$ ";
}
}
21 changes: 12 additions & 9 deletions src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,23 @@ export class TigrisShell {

/** Mount a bucket at a path. Throws if path is already mounted. */
mount(bucket: string, mountPoint: string): void {
if (this.mounts.some((m) => m.mountPoint === mountPoint)) {
throw new Error(`Already mounted at ${mountPoint}`);
const normalized = mountPoint.startsWith("/") ? mountPoint : `/${mountPoint}`;
if (this.mounts.some((m) => m.mountPoint === normalized)) {
throw new Error(`Already mounted at ${normalized}`);
}
const adapter = new TigrisAdapter(this.tigrisConfig, bucket);
this.mountableFs.mount(mountPoint, adapter);
this.mounts.push({ bucket, mountPoint, adapter });
this.mountableFs.mount(normalized, adapter);
this.mounts.push({ bucket, mountPoint: normalized, adapter });
}

/** Unmount a path. */
unmount(mountPoint: string): void {
const index = this.mounts.findIndex((m) => m.mountPoint === mountPoint);
const normalized = mountPoint.startsWith("/") ? mountPoint : `/${mountPoint}`;
const index = this.mounts.findIndex((m) => m.mountPoint === normalized);
if (index === -1) {
throw new Error(`No mount at ${mountPoint}`);
throw new Error(`No mount at ${normalized}`);
}
this.mountableFs.unmount(mountPoint);
this.mountableFs.unmount(normalized);
this.mounts.splice(index, 1);
}

Expand All @@ -84,9 +86,10 @@ export class TigrisShell {
/** Flush cached writes to Tigris. Flush all mounts or a specific path. */
async flush(mountPoint?: string): Promise<void> {
if (mountPoint !== undefined) {
const mount = this.mounts.find((m) => m.mountPoint === mountPoint);
const normalized = mountPoint.startsWith("/") ? mountPoint : `/${mountPoint}`;
const mount = this.mounts.find((m) => m.mountPoint === normalized);
if (!mount) {
throw new Error(`No mount at ${mountPoint}`);
throw new Error(`No mount at ${normalized}`);
}
return mount.adapter.flush();
}
Expand Down
Loading