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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/verda-cloud/verda-cli

go 1.25.10
go 1.25.11

require (
charm.land/lipgloss/v2 v2.0.2
Expand Down
29 changes: 15 additions & 14 deletions internal/skills/files/verda-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: Use when the user mentions Verda Cloud, GPU/CPU VMs, cloud instance
**Example:** `verda --agent instance-types --gpu -o json`

**NEVER do these:**
- NEVER run `verda` without `--agent -o json` (except `verda ssh` and `verda s3 configure`, which are interactive — tell the user to run those)
- NEVER run `verda` without `--agent -o json` (except `verda ssh` and `verda object-storage configure`, which are interactive — tell the user to run those)
- NEVER guess commands — consult the verda-reference skill or run `verda <cmd> --help`
- NEVER create resources without checking cost first
- NEVER delete/shutdown without explicit user confirmation
Expand All @@ -36,7 +36,7 @@ description: Use when the user mentions Verda Cloud, GPU/CPU VMs, cloud instance
| **VM Info** | "my VMs", "instances", "what's running", "what's offline" | `verda --agent vm list -o json` (add `--status` to filter). Use `vm describe <id>` for a specific VM |
| **Cost** | "balance", "burn rate", "spending", "how much" | `verda --agent cost balance -o json` and/or `cost running -o json` |
| **Storage** | "volumes", "disks", "block storage" | `verda --agent volume list -o json` |
| **Object Storage** | "bucket", "S3", "object storage", "upload a file", "download a file" | `verda --agent s3 ls -o json` (needs `s3 configure` first — see below) |
| **Object Storage** | "bucket", "S3", "object storage", "upload a file", "download a file" | `verda --agent object-storage ls -o json` (needs `object-storage configure` first — see below) |

### Explore — Use Specific Commands, Not `status`

Expand Down Expand Up @@ -78,24 +78,25 @@ Otherwise walk this chain. **ALWAYS** steps must run even if user specified valu

## Object Storage (S3)

S3-compatible object storage. **Separate credentials** from the main API —
keys are prefixed `verda_s3_` and set up by `verda s3 configure` (interactive,
S3-compatible object storage via the `object-storage` command (aliases `oss`, `d4`).
**Separate credentials** from the main API —
keys are prefixed `verda_s3_` and set up by `verda object-storage configure` (interactive,
user-only — like `auth login`; never run it yourself, never handle the keys).

1. **Check setup first:** `verda s3 show` (prints text, not JSON). If it shows `s3_configured: false` (or `access_key_loaded: false`), tell the user to run `verda s3 configure` (do NOT run it). Configured ⇔ `access_key_loaded: true`.
1. **Check setup first:** `verda object-storage show` (prints text, not JSON). If it shows `s3_configured: false` (or `access_key_loaded: false`), tell the user to run `verda object-storage configure` (do NOT run it). Configured ⇔ `access_key_loaded: true`.
2. **Then operate** (all support `--agent -o json`):

| Question / intent | Command |
|-------------------|---------|
| List buckets | `verda --agent s3 ls -o json` |
| List a bucket's contents | `verda --agent s3 ls s3://bucket -o json` (add `--recursive`) |
| Upload a file | `verda --agent s3 cp ./file s3://bucket/key -o json` |
| Download a file | `verda --agent s3 cp s3://bucket/key ./file -o json` |
| Copy / move within S3 | `verda --agent s3 cp\|mv s3://b/a s3://b/c -o json` |
| Mirror a directory | `verda --agent s3 sync ./dir s3://bucket/prefix/ -o json` |
| Delete object(s) | `verda --agent s3 rm s3://bucket/key --yes -o json` |
| Make / remove a bucket | `verda --agent s3 mb\|rb s3://bucket -o json` (`rb` needs `--yes`) |
| Time-limited share URL | `verda --agent s3 presign s3://bucket/key -o json` |
| List buckets | `verda --agent object-storage ls -o json` |
| List a bucket's contents | `verda --agent object-storage ls s3://bucket -o json` (add `--recursive`) |
| Upload a file | `verda --agent object-storage cp ./file s3://bucket/key -o json` |
| Download a file | `verda --agent object-storage cp s3://bucket/key ./file -o json` |
| Copy / move within S3 | `verda --agent object-storage cp\|mv s3://b/a s3://b/c -o json` |
| Mirror a directory | `verda --agent object-storage sync ./dir s3://bucket/prefix/ -o json` |
| Delete object(s) | `verda --agent object-storage rm s3://bucket/key --yes -o json` |
| Make / remove a bucket | `verda --agent object-storage mb\|rb s3://bucket -o json` (`rb` needs `--yes`) |
| Time-limited share URL | `verda --agent object-storage presign s3://bucket/key -o json` |

**Destructive (`rm`, `rb`):** require `--yes` in agent mode, else they return
`CONFIRMATION_REQUIRED`. `cp`/`mv`/`sync` don't prompt — confirm intent with the
Expand Down
43 changes: 22 additions & 21 deletions internal/skills/files/verda-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ All commands: `--agent -o json` (except `verda ssh` and `verda auth show`).
| "estimate", "how much will it cost" | `cost estimate` |
| "connect", "SSH in", "remote access" | Tell user to run `verda ssh <host>` themselves (interactive) |
| "login", "authenticate", "credentials" | `auth login` (user runs manually) |
| "bucket", "S3", "object storage", "list buckets" | `s3 ls` |
| "upload", "put file in bucket" | `s3 cp ./file s3://bucket/key` |
| "download", "get file from bucket" | `s3 cp s3://bucket/key ./file` |
| "sync", "mirror folder to/from bucket" | `s3 sync <src> <dst>` |
| "delete object", "remove from bucket" | `s3 rm s3://bucket/key --yes` |
| "make bucket", "create bucket" / "remove bucket" | `s3 mb` / `s3 rb --yes` |
| "share link", "presigned URL", "temporary link" | `s3 presign s3://bucket/key` |
| "set up S3", "configure object storage" | `s3 configure` (user runs manually — interactive) |
| "bucket", "S3", "object storage", "list buckets" | `object-storage ls` |
| "upload", "put file in bucket" | `object-storage cp ./file s3://bucket/key` |
| "download", "get file from bucket" | `object-storage cp s3://bucket/key ./file` |
| "sync", "mirror folder to/from bucket" | `object-storage sync <src> <dst>` |
| "delete object", "remove from bucket" | `object-storage rm s3://bucket/key --yes` |
| "make bucket", "create bucket" / "remove bucket" | `object-storage mb` / `object-storage rb --yes` |
| "share link", "presigned URL", "temporary link" | `object-storage presign s3://bucket/key` |
| "set up S3", "configure object storage" | `object-storage configure` (user runs manually — interactive) |

## Discovery

Expand Down Expand Up @@ -141,21 +141,22 @@ Hostname patterns: `{random}` → random words, `{location}` → location code

## Object Storage (S3)

The `object-storage` command (aliases `oss`, `d4`) manages S3-compatible storage.
Separate credentials from the main API (keys prefixed `verda_s3_`). Set up with
`verda s3 configure` (interactive — user runs it). Check status first:
`verda object-storage configure` (interactive — user runs it). Check status first:

| Command | Key Flags | Output Fields |
|---------|-----------|---------------|
| `verda s3 show` | `--profile` | Text key:value (NOT JSON): `s3_configured: false` only when unset; otherwise `access_key_loaded`, `secret_key_loaded`, `endpoint`, `region`. Configured ⇔ `access_key_loaded: true` |
| `verda s3 ls -o json` | — (lists buckets) | `buckets[]`: `name`, `created_at` |
| `verda s3 ls s3://bucket[/prefix] -o json` | `--recursive`, `--human-readable`, `--summarize` | `objects[]`: `key`, `size`, `modified`; `common_prefixes[]` |
| `verda s3 cp <src> <dst> -o json` | `--recursive`, `--include`, `--exclude`, `--content-type`, `--part-size`, `--concurrency`, `--no-resume`, `--dryrun` | `transfers[]`: `source`, `destination`, `bytes`, `status`; `summary` |
| `verda s3 mv <src> <dst> -o json` | same as `cp` (minus resume flags) | same as `cp` (`status: "moved"`) |
| `verda s3 rm s3://bucket/key -o json` | `--recursive`, `--include`, `--exclude`, `--dryrun`, **`--yes`** | `deleted[]`, `errors[]`, `dryrun` |
| `verda s3 sync <src> <dst> -o json` | `--delete`, `--exact-timestamps`, `--include`, `--exclude`, `--dryrun` | `transfers[]`, `deleted[]`, `summary` |
| `verda s3 mb s3://bucket -o json` | — | `bucket`, `created` |
| `verda s3 rb s3://bucket -o json` | `--force` (empty first), **`--yes`** | `bucket`, `removed`, `objects_deleted` |
| `verda s3 presign s3://bucket/key -o json` | `--expires-in` (e.g. `15m`, `24h`; default `1h`) | `url`, `expires_at` (table mode prints the bare URL to stdout) |
| `verda object-storage show` | `--profile` | Text key:value (NOT JSON): `s3_configured: false` only when unset; otherwise `access_key_loaded`, `secret_key_loaded`, `endpoint`, `region`. Configured ⇔ `access_key_loaded: true` |
| `verda object-storage ls -o json` | — (lists buckets) | `buckets[]`: `name`, `created_at` |
| `verda object-storage ls s3://bucket[/prefix] -o json` | `--recursive`, `--human-readable`, `--summarize` | `objects[]`: `key`, `size`, `modified`; `common_prefixes[]` |
| `verda object-storage cp <src> <dst> -o json` | `--recursive`, `--include`, `--exclude`, `--content-type`, `--part-size`, `--concurrency`, `--no-resume`, `--dryrun` | `transfers[]`: `source`, `destination`, `bytes`, `status`; `summary` |
| `verda object-storage mv <src> <dst> -o json` | same as `cp` (minus resume flags) | same as `cp` (`status: "moved"`) |
| `verda object-storage rm s3://bucket/key -o json` | `--recursive`, `--include`, `--exclude`, `--dryrun`, **`--yes`** | `deleted[]`, `errors[]`, `dryrun` |
| `verda object-storage sync <src> <dst> -o json` | `--delete`, `--exact-timestamps`, `--include`, `--exclude`, `--dryrun` | `transfers[]`, `deleted[]`, `summary` |
| `verda object-storage mb s3://bucket -o json` | — | `bucket`, `created` |
| `verda object-storage rb s3://bucket -o json` | `--force` (empty first), **`--yes`** | `bucket`, `removed`, `objects_deleted` |
| `verda object-storage presign s3://bucket/key -o json` | `--expires-in` (e.g. `15m`, `24h`; default `1h`) | `url`, `expires_at` (table mode prints the bare URL to stdout) |

Rules:
- **`src`/`dst`**: at least one must be an `s3://bucket/key` URI; the other may be a local path (upload/download) or another `s3://` URI (server-side copy).
Expand Down Expand Up @@ -193,5 +194,5 @@ Rules:
| volume ID | `volume list` | `id` |
| VM ID / hostname | `vm list` | `id`, `hostname` |
| template name | `template list` | `name` |
| bucket name | `s3 ls` | `buckets[].name` |
| object key | `s3 ls s3://bucket` | `objects[].key` |
| bucket name | `object-storage ls` | `buckets[].name` |
| object key | `object-storage ls s3://bucket` | `objects[].key` |
8 changes: 4 additions & 4 deletions internal/verda-cli/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import (
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/instancetypes"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/locations"
mcpcmd "github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/mcp"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/objectstorage"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/registry"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/s3"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/serverless"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/settings"
"github.com/verda-cloud/verda-cli/internal/verda-cli/cmd/skills"
Expand Down Expand Up @@ -81,7 +81,7 @@ func NewRootCommand(ioStreams cmdutil.IOStreams) (*cobra.Command, *clioptions.Op
}

// Commands that legitimately run without resolved credentials (mcp serve,
// auth show/use, registry/s3/skills trees, doctor).
// auth show/use, registry/object-storage/skills trees, doctor).
if skipCredentialResolution(cmd) {
log.Init(opts.Log)
return nil
Expand Down Expand Up @@ -136,7 +136,7 @@ func NewRootCommand(ioStreams cmdutil.IOStreams) (*cobra.Command, *clioptions.Op
instancetypes.NewCmdInstanceTypes(f, ioStreams),
locations.NewCmdLocations(f, ioStreams),
registry.NewCmdRegistry(f, ioStreams),
s3.NewCmdS3(f, ioStreams),
objectstorage.NewCmdObjectStorage(f, ioStreams),
sshkey.NewCmdSSHKey(f, ioStreams),
startupscript.NewCmdStartupScript(f, ioStreams),
template.NewCmdTemplate(f, ioStreams),
Expand Down Expand Up @@ -230,7 +230,7 @@ func skipCredentialResolution(cmd *cobra.Command) bool {
return true
case pName == "skills":
return true
case pName == "s3":
case pName == "object-storage":
return true
case pName == "registry":
return true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# S3 Command Knowledge

## Quick Reference
- Parent: `verda s3`
- Parent: `verda object-storage`
- Subcommands: `configure`, `show`, `ls`, `cp`, `mv`, `rm`, `sync`, `mb`, `rb`, `presign`
- Files:
- `s3.go` -- Parent command registration
Expand Down Expand Up @@ -76,9 +76,9 @@ Do NOT escape the whole `bucket/key` as a single string -- S3 rejects a pre-esca
- `aws-sdk-go-v2/feature/s3/manager` is deprecated upstream in favour of `feature/s3/transfermanager`. `//nolint:staticcheck` suppresses the warning in `transfer.go`. Swap to `transfermanager` when it ships a tagged release.
- Integration test (`tests/integration/s3_test.go`) is gated by BOTH a build tag (`integration`) AND env var (`VERDA_S3_INTEGRATION=1`). Default `make test` never runs it.
- `NewClient` assumes a non-nil `*options.S3Credentials`. `buildClientDefault` always passes a fresh `&options.S3Credentials{}` on load failure, to honour that contract rather than panic.
- `verda s3 show` with no S3 credentials still exits 0 (prints `s3_configured: false`). This is intentional -- it is a status command, not a validation command.
- `verda object-storage show` with no S3 credentials still exits 0 (prints `s3_configured: false`). This is intentional -- it is a status command, not a validation command.
- `uri.go` `Parse` accepts `s3://bucket` (empty key) as well as `s3://bucket/key/with/slashes`. Callers must decide whether an empty key is valid for their verb.
- `presign` writes the URL to stdout and the expiration hint to stderr so the output is safe to pipe (`verda s3 presign ... | pbcopy`).
- `presign` writes the URL to stdout and the expiration hint to stderr so the output is safe to pipe (`verda object-storage presign ... | pbcopy`).
- `sync --exact-timestamps` flips the mtime comparison from "newer source" to "different source"; otherwise near-identical mtimes can cause re-uploads.

## Relationships
Expand Down
Loading