diff --git a/README.md b/README.md index 4b98b6a..e78cd60 100644 --- a/README.md +++ b/README.md @@ -22,587 +22,440 @@ tigris [flags] Run `tigris help` to see all available commands, or `tigris help` for details on a specific command. -### Core Commands +### Commands -- `tigris ls [path]` - List all buckets (no arguments) or objects under a bucket/prefix path. Accepts bare names or t3:// URIs -- `tigris mk ` - Create a bucket (bare name) or a folder inside a bucket (bucket/folder/ with trailing slash) -- `tigris touch ` - Create an empty (zero-byte) object at the given bucket/key path -- `tigris cp ` - Copy files between local filesystem and Tigris, or between paths within Tigris. At least one side must be a remote t3:// path -- `tigris mv ` - Move (rename) objects within Tigris. Both source and destination must be remote t3:// paths -- `tigris rm ` - Remove a bucket, folder, or object from Tigris. A bare bucket name deletes the bucket itself -- `tigris stat [path]` - Show storage stats (no args), bucket info, or object metadata -- `tigris presign ` - Generate a presigned URL for temporary access to an object without credentials -- `tigris bundle ` - Download multiple objects as a streaming tar archive in a single request. Designed for batch workloads that need many objects without per-object HTTP overhead - -### Authentication - -- `tigris login` - Start a session via OAuth (default) or temporary credentials. Session state is cleared on logout -- `tigris logout` - End the current session and clear login state. Credentials saved via 'configure' are kept -- `tigris whoami` - Print the currently authenticated user, organization, and auth method -- `tigris configure` - Save access-key credentials to ~/.tigris/config.json for persistent use across all commands - -### Other - -- `tigris update` - Update the CLI to the latest version - -### Resources - -- `tigris organizations` - List, create, and switch between organizations. An organization is a workspace that contains your resources like buckets and access keys -- `tigris access-keys` - Create, list, inspect, delete, and assign roles to access keys. Access keys are credentials used for programmatic API access -- `tigris credentials` - Test whether your current credentials can reach Tigris and optionally verify access to a specific bucket -- `tigris buckets` - Create, inspect, update, and delete buckets. Buckets are top-level containers that hold objects -- `tigris forks` - (Deprecated, use "buckets create --fork-of" and "buckets list --forks-of") List and create forks -- `tigris snapshots` - List and take snapshots. A snapshot is a point-in-time, read-only copy of a bucket's state -- `tigris objects` - Low-level object operations for listing, downloading, uploading, and deleting individual objects in a bucket -- `tigris iam` - Identity and Access Management - manage policies, users, and permissions +| Command | Description | +|---------|-------------| +| `tigris configure` (c) | Save access-key credentials to ~/.tigris/config.json for persistent use across all commands | +| `tigris login` (l) | Start a session via OAuth (default) or temporary credentials. Session state is cleared on logout | +| `tigris whoami` (w) | Print the currently authenticated user, organization, and auth method | +| `tigris update` | Update the CLI to the latest version | +| `tigris logout` | End the current session and clear login state. Credentials saved via 'configure' are kept | +| `tigris credentials` (creds) | Test whether your current credentials can reach Tigris and optionally verify access to a specific bucket | +| `tigris ls` (list) | List all buckets (no arguments) or objects under a bucket/prefix path. Accepts bare names or t3:// URIs | +| `tigris mk` (create) | Create a bucket (bare name) or a folder inside a bucket (bucket/folder/ with trailing slash) | +| `tigris touch` | Create an empty (zero-byte) object at the given bucket/key path | +| `tigris stat` | Show storage stats (no args), bucket info, or object metadata | +| `tigris presign` | Generate a presigned URL for temporary access to an object without credentials | +| `tigris cp` (copy) | Copy files between local filesystem and Tigris, or between paths within Tigris. At least one side must be a remote t3:// path | +| `tigris mv` (move) | Move (rename) objects within Tigris. Both source and destination must be remote t3:// paths | +| `tigris rm` (remove) | Remove a bucket, folder, or object from Tigris. A bare bucket name deletes the bucket itself | +| `tigris bundle` | Download multiple objects as a streaming tar archive in a single request. Designed for batch workloads that need many objects without per-object HTTP overhead | +| `tigris organizations` (orgs) | List, create, and switch between organizations. An organization is a workspace that contains your resources like buckets and access keys | +| `tigris buckets` (b) | Create, inspect, update, and delete buckets. Buckets are top-level containers that hold objects | +| `tigris snapshots` (s) | List and take snapshots. A snapshot is a point-in-time, read-only copy of a bucket's state | +| `tigris objects` (o) | Low-level object operations for listing, downloading, uploading, and deleting individual objects in a bucket | +| `tigris access-keys` (keys) | Create, list, inspect, delete, and assign roles to access keys. Access keys are credentials used for programmatic API access | +| `tigris iam` | Identity and Access Management - manage policies, users, and permissions | --- -## Core Commands - -### `ls` | `list` - -List all buckets (no arguments) or objects under a bucket/prefix path. Accepts bare names or t3:// URIs +### `tigris configure` (c) -``` -tigris ls [path] [flags] -``` - -| Flag | Description | -|------|-------------| -| `-snapshot, --snapshot-version` | Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | -| `--format` | Output format | -| `--limit` | Maximum number of items to return per page | -| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | -| `--source` | List objects from a specific storage source on buckets with shadow migration enabled | - -**Examples:** -```bash -tigris ls -tigris ls my-bucket -tigris ls my-bucket/images/ -tigris ls t3://my-bucket/prefix/ -``` - -### `mk` | `create` - -Create a bucket (bare name) or a folder inside a bucket (bucket/folder/ with trailing slash) +Save access-key credentials to ~/.tigris/config.json for persistent use across all commands ``` -tigris mk [flags] +tigris configure [flags] ``` | Flag | Description | |------|-------------| -| `-a, --access` | Access level (only applies when creating a bucket) | -| `--public` | Shorthand for --access public (only applies when creating a bucket) | -| `-s, --enable-snapshots` | Enable snapshots for the bucket (only applies when creating a bucket) | -| `-t, --default-tier` | Default storage tier (only applies when creating a bucket) | -| `-c, --consistency` | (Deprecated, use --locations) Consistency level (only applies when creating a bucket) | -| `-r, --region` | (Deprecated, use --locations) Region (only applies when creating a bucket) | -| `-l, --locations` | Location for the bucket (only applies when creating a bucket) | -| `-fork, --fork-of` | Create this bucket as a fork (copy-on-write clone) of the named source bucket | -| `-source-snap, --source-snapshot` | Fork from a specific snapshot of the source bucket. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464). Requires --fork-of | +| `-key, --access-key` | Your Tigris access key ID | +| `-secret, --access-secret` | Your Tigris secret access key | +| `-e, --endpoint` | Tigris API endpoint (default: https://t3.storage.dev) | **Examples:** ```bash -tigris mk my-bucket -tigris mk my-bucket --access public --region iad -tigris mk my-bucket/images/ -tigris mk t3://my-bucket -tigris mk my-fork --fork-of my-bucket -tigris mk my-fork --fork-of my-bucket --source-snapshot 1765889000501544464 +tigris configure --access-key tid_AaBb --access-secret tsec_XxYy +tigris configure --endpoint https://custom.endpoint.dev ``` -### `touch` - -Create an empty (zero-byte) object at the given bucket/key path +### `tigris login` (l) -``` -tigris touch -``` +Start a session via OAuth (default) or temporary credentials. Session state is cleared on logout -**Examples:** -```bash -tigris touch my-bucket/placeholder.txt -tigris touch t3://my-bucket/logs/ -``` +| Command | Description | +|---------|-------------| +| `tigris login select` | Choose how to login - OAuth (browser) or credentials (access key) | +| `tigris login oauth` (o) | Login via browser using OAuth2 device flow. Best for interactive use | +| `tigris login credentials` (c) | Login with an access key and secret. Creates a temporary session that is cleared on logout | -### `cp` | `copy` +#### `tigris login select` -Copy files between local filesystem and Tigris, or between paths within Tigris. At least one side must be a remote t3:// path +Choose how to login - OAuth (browser) or credentials (access key) ``` -tigris cp [flags] -``` - -| Flag | Description | -|------|-------------| -| `-r, --recursive` | Copy directories recursively | - -**Examples:** -```bash -tigris cp ./file.txt t3://my-bucket/file.txt -tigris cp t3://my-bucket/file.txt ./local-copy.txt -tigris cp t3://my-bucket/src/ t3://my-bucket/dest/ -r -tigris cp ./images/ t3://my-bucket/images/ -r +tigris login select ``` -### `mv` | `move` +#### `tigris login oauth` (o) -Move (rename) objects within Tigris. Both source and destination must be remote t3:// paths +Login via browser using OAuth2 device flow. Best for interactive use ``` -tigris mv [flags] +tigris login oauth ``` -| Flag | Description | -|------|-------------| -| `-r, --recursive` | Move directories recursively | -| `-f, --force` | Skip confirmation prompts (alias for --yes) | - **Examples:** ```bash -tigris mv t3://my-bucket/old.txt t3://my-bucket/new.txt -f -tigris mv t3://my-bucket/old-dir/ t3://my-bucket/new-dir/ -rf -tigris mv my-bucket/a.txt my-bucket/b.txt -f +tigris login oauth ``` -### `rm` | `remove` +#### `tigris login credentials` (c) -Remove a bucket, folder, or object from Tigris. A bare bucket name deletes the bucket itself +Login with an access key and secret. Creates a temporary session that is cleared on logout ``` -tigris rm [flags] +tigris login credentials [flags] ``` | Flag | Description | |------|-------------| -| `-r, --recursive` | Remove directories recursively | -| `-f, --force` | Skip confirmation prompts (alias for --yes) | +| `-key, --access-key` | Your access key ID (will prompt if not provided) | +| `-secret, --access-secret` | Your secret access key (will prompt if not provided) | **Examples:** ```bash -tigris rm t3://my-bucket/file.txt -f -tigris rm t3://my-bucket/folder/ -rf -tigris rm t3://my-bucket -f -tigris rm "t3://my-bucket/logs/*.tmp" -f +tigris login credentials --access-key tid_AaBb --access-secret tsec_XxYy +tigris login credentials ``` -### `stat` +### `tigris whoami` (w) -Show storage stats (no args), bucket info, or object metadata +Print the currently authenticated user, organization, and auth method ``` -tigris stat [path] [flags] +tigris whoami ``` -| Flag | Description | -|------|-------------| -| `--format` | Output format | -| `-snapshot, --snapshot-version` | Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | - **Examples:** ```bash -tigris stat -tigris stat t3://my-bucket -tigris stat t3://my-bucket/my-object.json +tigris whoami ``` -### `presign` +### `tigris update` -Generate a presigned URL for temporary access to an object without credentials +Update the CLI to the latest version ``` -tigris presign [flags] +tigris update ``` -| Flag | Description | -|------|-------------| -| `-m, --method` | HTTP method for the presigned URL | -| `-e, --expires-in` | URL expiry time in seconds | -| `--access-key` | Access key ID to use for signing. If not provided, resolved from credentials or auto-selected | -| `--select` | Interactively select an access key (OAuth only) | -| `--format` | Output format | - **Examples:** ```bash -tigris presign my-bucket/file.txt -tigris presign t3://my-bucket/report.pdf --method put --expires-in 7200 -tigris presign my-bucket/image.png --format json -tigris presign my-bucket/data.csv --access-key tid_AaBb +tigris update ``` -### `bundle` +### `tigris logout` -Download multiple objects as a streaming tar archive in a single request. Designed for batch workloads that need many objects without per-object HTTP overhead +End the current session and clear login state. Credentials saved via 'configure' are kept ``` -tigris bundle [flags] +tigris logout ``` -| Flag | Description | -|------|-------------| -| `-k, --keys` | Comma-separated object keys, or path to a file with one key per line. If a local file matching the value exists, it is read as a keys file. If omitted, reads keys from stdin | -| `-o, --output` | Output file path. Defaults to stdout (for piping) | -| `--compression` | Compression algorithm for the archive. Auto-detected from output file extension when not specified | -| `--on-error` | How to handle missing objects. 'skip' omits them, 'fail' aborts the request | - **Examples:** ```bash -tigris bundle my-bucket --keys key1.jpg,key2.jpg --output archive.tar -tigris bundle my-bucket --keys keys.txt --output archive.tar -tigris bundle t3://my-bucket --keys keys.txt --compression gzip -o archive.tar.gz -cat keys.txt | tigris bundle my-bucket > archive.tar +tigris logout ``` -## Authentication +### `tigris credentials` (creds) -### `login` | `l` - -Start a session via OAuth (default) or temporary credentials. Session state is cleared on logout +Test whether your current credentials can reach Tigris and optionally verify access to a specific bucket | Command | Description | |---------|-------------| -| `login select` | Choose how to login - OAuth (browser) or credentials (access key) | -| `login oauth` (o) | Login via browser using OAuth2 device flow. Best for interactive use | -| `login credentials` (c) | Login with an access key and secret. Creates a temporary session that is cleared on logout | +| `tigris credentials test` (t) | Verify that current credentials are valid. Optionally checks access to a specific bucket | -#### `login select` +#### `tigris credentials test` (t) -``` -tigris login select -``` - -#### `login oauth` +Verify that current credentials are valid. Optionally checks access to a specific bucket ``` -tigris login oauth +tigris credentials test [flags] ``` +| Flag | Description | +|------|-------------| +| `-b, --bucket` | Bucket name to test access against (optional) | + **Examples:** ```bash -tigris login oauth +tigris credentials test +tigris credentials test --bucket my-bucket ``` -#### `login credentials` +### `tigris ls` (list) + +List all buckets (no arguments) or objects under a bucket/prefix path. Accepts bare names or t3:// URIs ``` -tigris login credentials [flags] +tigris ls [path] [flags] ``` | Flag | Description | |------|-------------| -| `-key, --access-key` | Your access key ID (will prompt if not provided) | -| `-secret, --access-secret` | Your secret access key (will prompt if not provided) | +| `-snapshot, --snapshot-version` | Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | +| `--format` | Output format (default: table) | +| `--limit` | Maximum number of items to return per page | +| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | +| `--source` | List objects from a specific storage source on buckets with shadow migration enabled | **Examples:** ```bash -tigris login credentials --access-key tid_AaBb --access-secret tsec_XxYy -tigris login credentials +tigris ls +tigris ls my-bucket +tigris ls my-bucket/images/ +tigris ls t3://my-bucket/prefix/ ``` -### `logout` +### `tigris mk` (create) -End the current session and clear login state. Credentials saved via 'configure' are kept +Create a bucket (bare name) or a folder inside a bucket (bucket/folder/ with trailing slash) ``` -tigris logout +tigris mk [flags] ``` +| Flag | Description | +|------|-------------| +| `-a, --access` | Access level (only applies when creating a bucket) (default: private) | +| `--public` | Shorthand for --access public (only applies when creating a bucket) | +| `-s, --enable-snapshots` | Enable snapshots for the bucket (only applies when creating a bucket) (default: false) | +| `-t, --default-tier` | Default storage tier (only applies when creating a bucket) (default: STANDARD) | +| `-l, --locations` | Location for the bucket (only applies when creating a bucket) (default: global) | +| `-fork, --fork-of` | Create this bucket as a fork (copy-on-write clone) of the named source bucket | +| `-source-snap, --source-snapshot` | Fork from a specific snapshot of the source bucket. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464). Requires --fork-of | + **Examples:** ```bash -tigris logout +tigris mk my-bucket +tigris mk my-bucket --access public --region iad +tigris mk my-bucket/images/ +tigris mk t3://my-bucket +tigris mk my-fork --fork-of my-bucket +tigris mk my-fork --fork-of my-bucket --source-snapshot 1765889000501544464 ``` -### `whoami` | `w` +### `tigris touch` -Print the currently authenticated user, organization, and auth method +Create an empty (zero-byte) object at the given bucket/key path ``` -tigris whoami +tigris touch ``` **Examples:** ```bash -tigris whoami +tigris touch my-bucket/placeholder.txt +tigris touch t3://my-bucket/logs/ ``` -### `configure` | `c` +### `tigris stat` -Save access-key credentials to ~/.tigris/config.json for persistent use across all commands +Show storage stats (no args), bucket info, or object metadata ``` -tigris configure [flags] +tigris stat [path] [flags] ``` | Flag | Description | |------|-------------| -| `-key, --access-key` | Your Tigris access key ID | -| `-secret, --access-secret` | Your Tigris secret access key | -| `-e, --endpoint` | Tigris API endpoint (default: https://t3.storage.dev) | +| `--format` | Output format (default: table) | +| `-snapshot, --snapshot-version` | Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | +| `--version-id` | Object version id to stat (requires bucket versioning). Omit to stat the latest version | **Examples:** ```bash -tigris configure --access-key tid_AaBb --access-secret tsec_XxYy -tigris configure --endpoint https://custom.endpoint.dev +tigris stat +tigris stat t3://my-bucket +tigris stat t3://my-bucket/my-object.json ``` -## Resources - -### `organizations` | `orgs` - -List, create, and switch between organizations. An organization is a workspace that contains your resources like buckets and access keys - -| Command | Description | -|---------|-------------| -| `organizations list` (l) | List all organizations you belong to and interactively select one as active | -| `organizations create` (c) | Create a new organization with the given name | -| `organizations select` (s) | Set the named organization as your active org for all subsequent commands | +### `tigris presign` -#### `organizations list` +Generate a presigned URL for temporary access to an object without credentials ``` -tigris organizations list [flags] +tigris presign [flags] ``` | Flag | Description | |------|-------------| -| `--format` | Output format (default: select) | -| `-i, --select` | Interactive selection mode | - -**Examples:** -```bash -tigris orgs list -tigris orgs list --format json -``` - -#### `organizations create` - -``` -tigris organizations create -``` - -**Examples:** -```bash -tigris orgs create my-org -``` - -#### `organizations select` - -``` -tigris organizations select -``` +| `-m, --method` | HTTP method for the presigned URL (default: get) | +| `-e, --expires-in` | URL expiry time in seconds (default: 3600) | +| `--access-key` | Access key ID to use for signing. If not provided, resolved from credentials or auto-selected | +| `--select` | Interactively select an access key (OAuth only) | +| `--format` | Output format (default: url) | **Examples:** ```bash -tigris orgs select my-org +tigris presign my-bucket/file.txt +tigris presign t3://my-bucket/report.pdf --method put --expires-in 7200 +tigris presign my-bucket/image.png --format json +tigris presign my-bucket/data.csv --access-key tid_AaBb ``` -### `access-keys` | `keys` - -Create, list, inspect, delete, and assign roles to access keys. Access keys are credentials used for programmatic API access +### `tigris cp` (copy) -| Command | Description | -|---------|-------------| -| `access-keys list` (l) | List all access keys in the current organization | -| `access-keys create` (c) | Create a new access key with the given name. Returns the key ID and secret (shown only once) | -| `access-keys delete` (d) | Permanently delete an access key by its ID. This revokes all access immediately | -| `access-keys get` (g) | Show details for an access key including its name, creation date, and assigned bucket roles | -| `access-keys assign` (a) | Assign per-bucket roles to an access key. Pair each --bucket with a --role (Editor or ReadOnly), or use --admin for org-wide access | -| `access-keys rotate` (r) | Rotate an access key's secret. The current secret is immediately invalidated and a new one is returned (shown only once) | -| `access-keys attach-policy` (ap) | Attach an IAM policy to an access key. If no policy ARN is provided, shows interactive selection of available policies | -| `access-keys detach-policy` (dp) | Detach an IAM policy from an access key. If no policy ARN is provided, shows interactive selection of attached policies | -| `access-keys list-policies` (lp) | List all IAM policies attached to an access key | - -#### `access-keys list` +Copy files between local filesystem and Tigris, or between paths within Tigris. At least one side must be a remote t3:// path ``` -tigris access-keys list [flags] +tigris cp [flags] ``` | Flag | Description | |------|-------------| -| `--format` | Output format (default: table) | -| `--limit` | Maximum number of items to return per page | -| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | +| `-r, --recursive` | Copy directories recursively | **Examples:** ```bash -tigris access-keys list -``` - -#### `access-keys create` - -``` -tigris access-keys create +tigris cp ./file.txt t3://my-bucket/file.txt +tigris cp t3://my-bucket/file.txt ./local-copy.txt +tigris cp t3://my-bucket/src/ t3://my-bucket/dest/ -r +tigris cp ./images/ t3://my-bucket/images/ -r ``` -**Examples:** -```bash -tigris access-keys create my-ci-key -``` +### `tigris mv` (move) -#### `access-keys delete` +Move (rename) objects within Tigris. Both source and destination must be remote t3:// paths ``` -tigris access-keys delete [flags] +tigris mv [flags] ``` | Flag | Description | |------|-------------| -| `--force` | Skip confirmation prompts (alias for --yes) | +| `-r, --recursive` | Move directories recursively | +| `-f, --force` | Skip confirmation prompts (alias for --yes) | **Examples:** ```bash -tigris access-keys delete tid_AaBbCcDdEeFf --yes -``` - -#### `access-keys get` - -``` -tigris access-keys get +tigris mv t3://my-bucket/old.txt t3://my-bucket/new.txt -f +tigris mv t3://my-bucket/old-dir/ t3://my-bucket/new-dir/ -rf +tigris mv my-bucket/a.txt my-bucket/b.txt -f ``` -**Examples:** -```bash -tigris access-keys get tid_AaBbCcDdEeFf -``` +### `tigris rm` (remove) -#### `access-keys assign` +Remove a bucket, folder, or object from Tigris. A bare bucket name deletes the bucket itself ``` -tigris access-keys assign [flags] +tigris rm [flags] ``` | Flag | Description | |------|-------------| -| `-b, --bucket` | Bucket name (can specify multiple, comma-separated). Each bucket is paired positionally with a --role value | -| `-r, --role` | Role to assign (can specify multiple, comma-separated). Each role pairs with the corresponding --bucket value | -| `--admin` | Grant admin access to all buckets in the organization | -| `--revoke-roles` | Revoke all bucket roles from the access key | +| `-r, --recursive` | Remove directories recursively | +| `-f, --force` | Skip confirmation prompts (alias for --yes) | **Examples:** ```bash -tigris access-keys assign tid_AaBb --bucket my-bucket --role Editor -tigris access-keys assign tid_AaBb --bucket a,b --role Editor,ReadOnly -tigris access-keys assign tid_AaBb --admin -tigris access-keys assign tid_AaBb --revoke-roles +tigris rm t3://my-bucket/file.txt -f +tigris rm t3://my-bucket/folder/ -rf +tigris rm t3://my-bucket -f +tigris rm "t3://my-bucket/logs/*.tmp" -f ``` -#### `access-keys rotate` +### `tigris bundle` + +Download multiple objects as a streaming tar archive in a single request. Designed for batch workloads that need many objects without per-object HTTP overhead ``` -tigris access-keys rotate [flags] +tigris bundle [flags] ``` | Flag | Description | |------|-------------| -| `--force` | Skip confirmation prompts (alias for --yes) | +| `-k, --keys` | Comma-separated object keys, or path to a file with one key per line. If a local file matching the value exists, it is read as a keys file. If omitted, reads keys from stdin | +| `-o, --output` | Output file path. Defaults to stdout (for piping) | +| `--compression` | Compression algorithm for the archive. Auto-detected from output file extension when not specified | +| `--on-error` | How to handle missing objects. 'skip' omits them, 'fail' aborts the request (default: skip) | **Examples:** ```bash -tigris access-keys rotate tid_AaBbCcDdEeFf --yes +tigris bundle my-bucket --keys key1.jpg,key2.jpg --output archive.tar +tigris bundle my-bucket --keys keys.txt --output archive.tar +tigris bundle t3://my-bucket --keys keys.txt --compression gzip -o archive.tar.gz +cat keys.txt | tigris bundle my-bucket > archive.tar ``` -#### `access-keys attach-policy` +### `tigris organizations` (orgs) -``` -tigris access-keys attach-policy [flags] -``` +List, create, and switch between organizations. An organization is a workspace that contains your resources like buckets and access keys -| Flag | Description | -|------|-------------| -| `--policy-arn` | ARN of the policy to attach | +| Command | Description | +|---------|-------------| +| `tigris organizations list` (l) | List all organizations you belong to and interactively select one as active | +| `tigris organizations create` (c) | Create a new organization with the given name | +| `tigris organizations select` (s) | Set the named organization as your active org for all subsequent commands | -**Examples:** -```bash -tigris access-keys attach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy -tigris access-keys attach-policy tid_AaBb -``` +#### `tigris organizations list` (l) -#### `access-keys detach-policy` +List all organizations you belong to and interactively select one as active ``` -tigris access-keys detach-policy [flags] +tigris organizations list [flags] ``` | Flag | Description | |------|-------------| -| `--policy-arn` | ARN of the policy to detach | -| `--force` | Skip confirmation prompts (alias for --yes) | +| `--format` | Output format (default: select) | +| `-i, --select` | Interactive selection mode | **Examples:** ```bash -tigris access-keys detach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy --yes -tigris access-keys detach-policy tid_AaBb +tigris orgs list +tigris orgs list --format json ``` -#### `access-keys list-policies` +#### `tigris organizations create` (c) + +Create a new organization with the given name ``` -tigris access-keys list-policies [flags] +tigris organizations create ``` -| Flag | Description | -|------|-------------| -| `--format` | Output format (default: table) | -| `--limit` | Maximum number of items to return per page | -| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | - **Examples:** ```bash -tigris access-keys list-policies tid_AaBbCcDdEeFf +tigris orgs create my-org ``` -### `credentials` | `creds` +#### `tigris organizations select` (s) -Test whether your current credentials can reach Tigris and optionally verify access to a specific bucket - -| Command | Description | -|---------|-------------| -| `credentials test` (t) | Verify that current credentials are valid. Optionally checks access to a specific bucket | - -#### `credentials test` +Set the named organization as your active org for all subsequent commands ``` -tigris credentials test [flags] +tigris organizations select ``` -| Flag | Description | -|------|-------------| -| `-b, --bucket` | Bucket name to test access against (optional) | - **Examples:** ```bash -tigris credentials test -tigris credentials test --bucket my-bucket +tigris orgs select my-org ``` -### Buckets - -Buckets are containers for objects. You can also create forks and snapshots of buckets. - -#### `buckets` | `b` +### `tigris buckets` (b) Create, inspect, update, and delete buckets. Buckets are top-level containers that hold objects | Command | Description | |---------|-------------| -| `buckets list` (l) | List all buckets in the current organization | -| `buckets create` (c) | Create a new bucket with optional access, tier, and location settings | -| `buckets get` (g) | Show details for a bucket including access level, region, tier, and custom domain | -| `buckets delete` (d) | Delete one or more buckets by name. The bucket must be empty or delete-protection must be off | -| `buckets set` (s) | Update settings on an existing bucket such as access level, location, caching, or custom domain | -| `buckets set-ttl` | Configure object expiration (TTL) on a bucket. Objects expire after a number of days or on a specific date | -| `buckets set-locations` | Set the data locations for a bucket | -| `buckets set-migration` | Configure data migration from an external S3-compatible source bucket. Tigris will pull objects on demand from the source | -| `buckets migrate` | Actively migrate all objects from a shadow bucket to Tigris by scheduling server-side migration for unmigrated objects | -| `buckets set-transition` | Configure a lifecycle transition rule on a bucket. Automatically move objects to a different storage class after a number of days or on a specific date | -| `buckets set-notifications` | Configure object event notifications on a bucket. Sends webhook requests to a URL when objects are created, updated, or deleted | -| `buckets set-cors` | Configure CORS rules on a bucket. Each invocation adds a rule unless --override or --reset is used | - -##### `buckets list` +| `tigris buckets list` (l) | List all buckets in the current organization | +| `tigris buckets create` (c) | Create a new bucket with optional access, tier, and location settings | +| `tigris buckets get` (g) | Show details for a bucket including access level, region, tier, and custom domain | +| `tigris buckets delete` (d) | Delete one or more buckets by name. The bucket must be empty or delete-protection must be off | +| `tigris buckets set` (s) | Update settings on an existing bucket such as access level, location, caching, or custom domain | +| `tigris buckets set-locations` | Set the data locations for a bucket | +| `tigris buckets set-migration` | Configure data migration from an external S3-compatible source bucket. Tigris will pull objects on demand from the source | +| `tigris buckets migrate` | Actively migrate all objects from a shadow bucket to Tigris by scheduling server-side migration for unmigrated objects | +| `tigris buckets lifecycle` (lc) | Manage bucket lifecycle rules. Each rule combines an optional storage-class transition and/or expiration (TTL), scoped to an optional key prefix | +| `tigris buckets set-notifications` | Configure object event notifications on a bucket. Sends webhook requests to a URL when objects are created, updated, or deleted | +| `tigris buckets set-cors` | Configure CORS rules on a bucket. Each invocation adds a rule unless --override or --reset is used | + +#### `tigris buckets list` (l) + +List all buckets in the current organization ``` tigris buckets list [flags] @@ -622,7 +475,9 @@ tigris buckets list --format json tigris buckets list --forks-of my-bucket ``` -##### `buckets create` +#### `tigris buckets create` (c) + +Create a new bucket with optional access, tier, and location settings ``` tigris buckets create [name] [flags] @@ -634,8 +489,6 @@ tigris buckets create [name] [flags] | `--public` | Shorthand for --access public | | `-s, --enable-snapshots` | Enable snapshots for the bucket (default: false) | | `-t, --default-tier` | Choose the default tier for the bucket (default: STANDARD) | -| `-c, --consistency` | (Deprecated, use --locations) Choose the consistency level for the bucket | -| `-r, --region` | (Deprecated, use --locations) Region | | `-l, --locations` | Location for the bucket (default: global) | | `-fork, --fork-of` | Create this bucket as a fork (copy-on-write clone) of the named source bucket | | `-source-snap, --source-snapshot` | Fork from a specific snapshot of the source bucket. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464). Requires --fork-of | @@ -649,7 +502,9 @@ tigris buckets create my-fork --fork-of my-bucket tigris buckets create my-fork --fork-of my-bucket --source-snapshot 1765889000501544464 ``` -##### `buckets get` +#### `tigris buckets get` (g) + +Show details for a bucket including access level, region, tier, and custom domain ``` tigris buckets get [flags] @@ -664,7 +519,9 @@ tigris buckets get [flags] tigris buckets get my-bucket ``` -##### `buckets delete` +#### `tigris buckets delete` (d) + +Delete one or more buckets by name. The bucket must be empty or delete-protection must be off ``` tigris buckets delete [flags] @@ -680,7 +537,9 @@ tigris buckets delete my-bucket --yes tigris buckets delete bucket-a,bucket-b --yes ``` -##### `buckets set` +#### `tigris buckets set` (s) + +Update settings on an existing bucket such as access level, location, caching, or custom domain ``` tigris buckets set [flags] @@ -689,7 +548,6 @@ tigris buckets set [flags] | Flag | Description | |------|-------------| | `--access` | Bucket access level | -| `--region` | (Deprecated, use --locations) Allowed regions (can specify multiple) | | `--locations` | Bucket location (see https://www.tigrisdata.com/docs/buckets/locations/ for more details) | | `--allow-object-acl` | Enable object-level ACL | | `--disable-directory-listing` | Disable directory listing | @@ -705,27 +563,9 @@ tigris buckets set my-bucket --locations iad,fra --cache-control 'max-age=3600' tigris buckets set my-bucket --custom-domain assets.example.com ``` -##### `buckets set-ttl` +#### `tigris buckets set-locations` -``` -tigris buckets set-ttl [flags] -``` - -| Flag | Description | -|------|-------------| -| `-d, --days` | Expire objects after this many days | -| `--date` | Expire objects on this date (ISO-8601, e.g. 2026-06-01) | -| `--enable` | Enable TTL on the bucket (uses existing lifecycle rules) | -| `--disable` | Disable TTL on the bucket | - -**Examples:** -```bash -tigris buckets set-ttl my-bucket --days 30 -tigris buckets set-ttl my-bucket --date 2026-06-01 -tigris buckets set-ttl my-bucket --disable -``` - -##### `buckets set-locations` +Set the data locations for a bucket ``` tigris buckets set-locations [flags] @@ -742,7 +582,9 @@ tigris buckets set-locations my-bucket --locations iad,fra tigris buckets set-locations my-bucket --locations global ``` -##### `buckets set-migration` +#### `tigris buckets set-migration` + +Configure data migration from an external S3-compatible source bucket. Tigris will pull objects on demand from the source ``` tigris buckets set-migration [flags] @@ -760,47 +602,109 @@ tigris buckets set-migration [flags] **Examples:** ```bash -tigris buckets set-migration my-bucket --bucket source-bucket --endpoint https://s3.amazonaws.com --region us-east-1 --access-key AKIA... --secret-key wJal... -tigris buckets set-migration my-bucket --bucket source-bucket --endpoint https://s3.amazonaws.com --region us-east-1 --access-key AKIA... --secret-key wJal... --write-through -tigris buckets set-migration my-bucket --disable +tigris buckets set-migration my-bucket --bucket source-bucket --endpoint https://s3.amazonaws.com --region us-east-1 --access-key AKIA... --secret-key wJal... +tigris buckets set-migration my-bucket --bucket source-bucket --endpoint https://s3.amazonaws.com --region us-east-1 --access-key AKIA... --secret-key wJal... --write-through +tigris buckets set-migration my-bucket --disable +``` + +#### `tigris buckets migrate` + +Actively migrate all objects from a shadow bucket to Tigris by scheduling server-side migration for unmigrated objects + +``` +tigris buckets migrate +``` + +**Examples:** +```bash +tigris buckets migrate my-bucket +tigris buckets migrate my-bucket/images/ +tigris buckets migrate t3://my-bucket/prefix/ +``` + +#### `tigris buckets lifecycle` (lc) + +Manage bucket lifecycle rules. Each rule combines an optional storage-class transition and/or expiration (TTL), scoped to an optional key prefix + +| Command | Description | +|---------|-------------| +| `tigris buckets lifecycle list` (l) | List lifecycle rules on a bucket | +| `tigris buckets lifecycle create` (c) | Create a new lifecycle rule. A rule must include a transition (--storage-class with --days or --date) and/or an expiration (--expire-days or --expire-date), and may optionally be scoped via --prefix | +| `tigris buckets lifecycle edit` (e) | Edit an existing lifecycle rule by its id. Only specified fields are changed | + +##### `tigris buckets lifecycle list` (l) + +List lifecycle rules on a bucket + +``` +tigris buckets lifecycle list [flags] +``` + +| Flag | Description | +|------|-------------| +| `--format` | Output format (default: table) | + +**Examples:** +```bash +tigris buckets lifecycle list my-bucket +tigris buckets lifecycle list my-bucket --json ``` -##### `buckets migrate` +##### `tigris buckets lifecycle create` (c) + +Create a new lifecycle rule. A rule must include a transition (--storage-class with --days or --date) and/or an expiration (--expire-days or --expire-date), and may optionally be scoped via --prefix ``` -tigris buckets migrate +tigris buckets lifecycle create [flags] ``` +| Flag | Description | +|------|-------------| +| `-p, --prefix` | Key prefix to scope the rule to. Omit for a bucket-wide rule | +| `-s, --storage-class` | Target storage class for the transition | +| `-d, --days` | Transition objects after this many days (used with --storage-class) | +| `--date` | Transition objects on this date (ISO-8601, e.g. 2026-06-01) (used with --storage-class) | +| `--expire-days` | Expire (delete) objects after this many days | +| `--expire-date` | Expire (delete) objects on this date (ISO-8601, e.g. 2026-06-01) | +| `--disable` | Create the rule in a disabled state | + **Examples:** ```bash -tigris buckets migrate my-bucket -tigris buckets migrate my-bucket/images/ -tigris buckets migrate t3://my-bucket/prefix/ +tigris buckets lifecycle create my-bucket --storage-class STANDARD_IA --days 30 +tigris buckets lifecycle create my-bucket --prefix logs/ --storage-class GLACIER --days 90 +tigris buckets lifecycle create my-bucket --prefix tmp/ --expire-days 7 +tigris buckets lifecycle create my-bucket --prefix archive/ --storage-class GLACIER --days 30 --expire-days 365 ``` -##### `buckets set-transition` +##### `tigris buckets lifecycle edit` (e) + +Edit an existing lifecycle rule by its id. Only specified fields are changed ``` -tigris buckets set-transition [flags] +tigris buckets lifecycle edit [flags] ``` | Flag | Description | |------|-------------| -| `-s, --storage-class` | Target storage class to transition objects to | -| `-d, --days` | Transition objects after this many days | -| `--date` | Transition objects on this date (ISO-8601, e.g. 2026-06-01) | -| `--enable` | Enable lifecycle transition rules on the bucket | -| `--disable` | Disable lifecycle transition rules on the bucket | +| `-p, --prefix` | Replace the rule's key prefix | +| `-s, --storage-class` | Replace the rule's transition target | +| `-d, --days` | Replace the rule's transition days | +| `--date` | Replace the rule's transition date (ISO-8601) | +| `--expire-days` | Replace the rule's expiration days | +| `--expire-date` | Replace the rule's expiration date (ISO-8601) | +| `--enable` | Enable the rule | +| `--disable` | Disable the rule (does not remove it) | **Examples:** ```bash -tigris buckets set-transition my-bucket --storage-class STANDARD_IA --days 30 -tigris buckets set-transition my-bucket --storage-class GLACIER --date 2026-06-01 -tigris buckets set-transition my-bucket --enable -tigris buckets set-transition my-bucket --disable +tigris buckets lifecycle edit my-bucket abc123 --days 60 +tigris buckets lifecycle edit my-bucket abc123 --expire-days 90 +tigris buckets lifecycle edit my-bucket abc123 --enable ``` -##### `buckets set-notifications` +#### `tigris buckets set-notifications` + +Configure object event notifications on a bucket. Sends webhook requests to a URL when objects are created, updated, or deleted ``` tigris buckets set-notifications [flags] @@ -828,7 +732,9 @@ tigris buckets set-notifications my-bucket --disable tigris buckets set-notifications my-bucket --reset ``` -##### `buckets set-cors` +#### `tigris buckets set-cors` + +Configure CORS rules on a bucket. Each invocation adds a rule unless --override or --reset is used ``` tigris buckets set-cors [flags] @@ -852,57 +758,18 @@ tigris buckets set-cors my-bucket --origins https://example.com --override tigris buckets set-cors my-bucket --reset ``` -#### `forks` | `f` - -(Deprecated, use "buckets create --fork-of" and "buckets list --forks-of") List and create forks - -| Command | Description | -|---------|-------------| -| `forks list` (l) | (Deprecated, use "buckets list --forks-of") List all forks created from the given source bucket | -| `forks create` (c) | (Deprecated, use "buckets create --fork-of") Create a new fork (copy-on-write clone) of the source bucket | - -##### `forks list` - -``` -tigris forks list [flags] -``` - -| Flag | Description | -|------|-------------| -| `--format` | Output format (default: table) | - -**Examples:** -```bash -tigris forks list my-bucket -tigris forks list my-bucket --format json -``` - -##### `forks create` - -``` -tigris forks create [flags] -``` - -| Flag | Description | -|------|-------------| -| `-s, --snapshot` | Create fork from a specific snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | - -**Examples:** -```bash -tigris forks create my-bucket my-fork -tigris forks create my-bucket my-fork --snapshot 1765889000501544464 -``` - -#### `snapshots` | `s` +### `tigris snapshots` (s) List and take snapshots. A snapshot is a point-in-time, read-only copy of a bucket's state | Command | Description | |---------|-------------| -| `snapshots list` (l) | List all snapshots for the given bucket, ordered by creation time | -| `snapshots take` (t) | Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot | +| `tigris snapshots list` (l) | List all snapshots for the given bucket, ordered by creation time | +| `tigris snapshots take` (t) | Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot | + +#### `tigris snapshots list` (l) -##### `snapshots list` +List all snapshots for the given bucket, ordered by creation time ``` tigris snapshots list [flags] @@ -920,7 +787,9 @@ tigris snapshots list my-bucket tigris snapshots list my-bucket --format json ``` -##### `snapshots take` +#### `tigris snapshots take` (t) + +Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot ``` tigris snapshots take [snapshot-name] @@ -932,20 +801,24 @@ tigris snapshots take my-bucket tigris snapshots take my-bucket my-snapshot ``` -### `objects` | `o` +### `tigris objects` (o) Low-level object operations for listing, downloading, uploading, and deleting individual objects in a bucket | Command | Description | |---------|-------------| -| `objects list` (l) | List objects in a bucket, optionally filtered by a key prefix | -| `objects get` (g) | Download an object by key. Prints to stdout by default, or saves to a file with --output | -| `objects put` (p) | Upload a local file as an object. Content-type is auto-detected from extension unless overridden | -| `objects delete` (d) | Delete one or more objects by key from the given bucket | -| `objects set` (s) | Update settings on an existing object such as access level | -| `objects info` (i) | Show metadata for an object (content type, size, modified date) | +| `tigris objects list` (l) | List objects in a bucket, optionally filtered by a key prefix | +| `tigris objects list-versions` (lv) | List object versions and delete markers in a bucket (requires bucket versioning). Returns both arrays separately to match the S3 ListObjectVersions response | +| `tigris objects get` (g) | Download an object by key. Prints to stdout by default, or saves to a file with --output | +| `tigris objects put` (p) | Upload a local file as an object. Content-type is auto-detected from extension unless overridden | +| `tigris objects delete` (d) | Delete one or more objects by key from the given bucket. On a versioned bucket, the default creates a delete marker; use --version-id or --all-versions to hard-delete versions | +| `tigris objects set` (s) | (Deprecated) Update settings on an existing object such as access level. Use `tigris objects set-access` for ACL changes and `tigris mv` to rename | +| `tigris objects set-access` (sa) | Set the access level (public or private) on an existing object | +| `tigris objects info` (i) | Show metadata for an object (content type, size, modified date) | + +#### `tigris objects list` (l) -#### `objects list` +List objects in a bucket, optionally filtered by a key prefix ``` tigris objects list [flags] @@ -969,7 +842,34 @@ tigris objects list my-bucket --prefix images/ tigris objects list my-bucket --format json ``` -#### `objects get` +#### `tigris objects list-versions` (lv) + +List object versions and delete markers in a bucket (requires bucket versioning). Returns both arrays separately to match the S3 ListObjectVersions response + +``` +tigris objects list-versions [flags] +``` + +| Flag | Description | +|------|-------------| +| `-p, --prefix` | Filter by key prefix | +| `-d, --delimiter` | Group keys sharing a common prefix up to the delimiter (e.g. "/" for folder-style grouping) | +| `--format` | Output format (default: table) | +| `--limit` | Maximum number of items to return per page | +| `--key-marker` | Pagination marker — the key to start listing from (from a prior nextKeyMarker) | +| `--version-id-marker` | Pagination marker — the version id to start listing from (from a prior nextVersionIdMarker) | + +**Examples:** +```bash +tigris objects list-versions my-bucket +tigris objects list-versions t3://my-bucket/logs/ +tigris objects list-versions my-bucket --prefix images/ +tigris objects list-versions my-bucket --format json +``` + +#### `tigris objects get` (g) + +Download an object by key. Prints to stdout by default, or saves to a file with --output ``` tigris objects get [key] [flags] @@ -980,6 +880,7 @@ tigris objects get [key] [flags] | `-o, --output` | Output file path (if not specified, prints to stdout) | | `-m, --mode` | Response mode: "string" loads into memory, "stream" writes in chunks (auto-detected from extension if not specified) | | `-snapshot, --snapshot-version` | Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) | +| `--version-id` | Object version id to download (requires bucket versioning). Omit to download the latest version | **Examples:** ```bash @@ -988,7 +889,9 @@ tigris objects get t3://my-bucket/config.json tigris objects get my-bucket archive.zip --output ./archive.zip --mode stream ``` -#### `objects put` +#### `tigris objects put` (p) + +Upload a local file as an object. Content-type is auto-detected from extension unless overridden ``` tigris objects put [key] [file] [flags] @@ -1007,7 +910,9 @@ tigris objects put t3://my-bucket/report.pdf ./report.pdf tigris objects put my-bucket logo.png ./logo.png --access public --content-type image/png ``` -#### `objects delete` +#### `tigris objects delete` (d) + +Delete one or more objects by key from the given bucket. On a versioned bucket, the default creates a delete marker; use --version-id or --all-versions to hard-delete versions ``` tigris objects delete [key] [flags] @@ -1015,6 +920,8 @@ tigris objects delete [key] [flags] | Flag | Description | |------|-------------| +| `--version-id` | Hard-delete a specific object version (requires bucket versioning). Targets a single key | +| `--all-versions` | Hard-delete every version and delete marker for the given key(s). Mutually exclusive with --version-id | | `--force` | Skip confirmation prompts (alias for --yes) | **Examples:** @@ -1022,9 +929,13 @@ tigris objects delete [key] [flags] tigris objects delete my-bucket old-file.txt --yes tigris objects delete t3://my-bucket/old-file.txt --yes tigris objects delete my-bucket file-a.txt,file-b.txt --yes +tigris objects delete my-bucket old-file.txt --version-id abc123 --yes +tigris objects delete my-bucket old-file.txt --all-versions --yes ``` -#### `objects set` +#### `tigris objects set` (s) + +(Deprecated) Update settings on an existing object such as access level. Use `tigris objects set-access` for ACL changes and `tigris mv` to rename ``` tigris objects set [key] [flags] @@ -1042,7 +953,27 @@ tigris objects set t3://my-bucket/my-file.txt --access public tigris objects set my-bucket my-file.txt --access private ``` -#### `objects info` +#### `tigris objects set-access` (sa) + +Set the access level (public or private) on an existing object + +``` +tigris objects set-access [key] [access] [flags] +``` + +| Flag | Description | +|------|-------------| +| `--format` | Output format (default: table) | + +**Examples:** +```bash +tigris objects set-access my-bucket my-file.txt public +tigris objects set-access t3://my-bucket/my-file.txt private +``` + +#### `tigris objects info` (i) + +Show metadata for an object (content type, size, modified date) ``` tigris objects info [key] [flags] @@ -1052,39 +983,217 @@ tigris objects info [key] [flags] |------|-------------| | `--format` | Output format (default: table) | | `-snapshot, --snapshot-version` | Read from a specific bucket snapshot | +| `--version-id` | Object version id (requires bucket versioning). Omit to read the latest version | **Examples:** ```bash tigris objects info my-bucket report.pdf tigris objects info t3://my-bucket/report.pdf tigris objects info my-bucket report.pdf --format json +tigris objects info my-bucket report.pdf --version-id abc123 +``` + +### `tigris access-keys` (keys) + +Create, list, inspect, delete, and assign roles to access keys. Access keys are credentials used for programmatic API access + +| Command | Description | +|---------|-------------| +| `tigris access-keys list` (l) | List all access keys in the current organization | +| `tigris access-keys create` (c) | Create a new access key with the given name. Returns the key ID and secret (shown only once) | +| `tigris access-keys delete` (d) | Permanently delete an access key by its ID. This revokes all access immediately | +| `tigris access-keys get` (g) | Show details for an access key including its name, creation date, and assigned bucket roles | +| `tigris access-keys assign` (a) | Assign per-bucket roles to an access key. Pair each --bucket with a --role (Editor or ReadOnly), or use --admin for org-wide access | +| `tigris access-keys rotate` (r) | Rotate an access key's secret. The current secret is immediately invalidated and a new one is returned (shown only once) | +| `tigris access-keys attach-policy` (ap) | Attach an IAM policy to an access key. If no policy ARN is provided, shows interactive selection of available policies | +| `tigris access-keys detach-policy` (dp) | Detach an IAM policy from an access key. If no policy ARN is provided, shows interactive selection of attached policies | +| `tigris access-keys list-policies` (lp) | List all IAM policies attached to an access key | + +#### `tigris access-keys list` (l) + +List all access keys in the current organization + +``` +tigris access-keys list [flags] +``` + +| Flag | Description | +|------|-------------| +| `--format` | Output format (default: table) | +| `--limit` | Maximum number of items to return per page | +| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | + +**Examples:** +```bash +tigris access-keys list +``` + +#### `tigris access-keys create` (c) + +Create a new access key with the given name. Returns the key ID and secret (shown only once) + +``` +tigris access-keys create +``` + +**Examples:** +```bash +tigris access-keys create my-ci-key +``` + +#### `tigris access-keys delete` (d) + +Permanently delete an access key by its ID. This revokes all access immediately + +``` +tigris access-keys delete [flags] +``` + +| Flag | Description | +|------|-------------| +| `--force` | Skip confirmation prompts (alias for --yes) | + +**Examples:** +```bash +tigris access-keys delete tid_AaBbCcDdEeFf --yes +``` + +#### `tigris access-keys get` (g) + +Show details for an access key including its name, creation date, and assigned bucket roles + +``` +tigris access-keys get +``` + +**Examples:** +```bash +tigris access-keys get tid_AaBbCcDdEeFf +``` + +#### `tigris access-keys assign` (a) + +Assign per-bucket roles to an access key. Pair each --bucket with a --role (Editor or ReadOnly), or use --admin for org-wide access + +``` +tigris access-keys assign [flags] +``` + +| Flag | Description | +|------|-------------| +| `-b, --bucket` | Bucket name (can specify multiple, comma-separated). Each bucket is paired positionally with a --role value | +| `-r, --role` | Role to assign (can specify multiple, comma-separated). Each role pairs with the corresponding --bucket value | +| `--admin` | Grant admin access to all buckets in the organization | +| `--revoke-roles` | Revoke all bucket roles from the access key | + +**Examples:** +```bash +tigris access-keys assign tid_AaBb --bucket my-bucket --role Editor +tigris access-keys assign tid_AaBb --bucket a,b --role Editor,ReadOnly +tigris access-keys assign tid_AaBb --admin +tigris access-keys assign tid_AaBb --revoke-roles +``` + +#### `tigris access-keys rotate` (r) + +Rotate an access key's secret. The current secret is immediately invalidated and a new one is returned (shown only once) + +``` +tigris access-keys rotate [flags] +``` + +| Flag | Description | +|------|-------------| +| `--force` | Skip confirmation prompts (alias for --yes) | + +**Examples:** +```bash +tigris access-keys rotate tid_AaBbCcDdEeFf --yes +``` + +#### `tigris access-keys attach-policy` (ap) + +Attach an IAM policy to an access key. If no policy ARN is provided, shows interactive selection of available policies + +``` +tigris access-keys attach-policy [flags] +``` + +| Flag | Description | +|------|-------------| +| `--policy-arn` | ARN of the policy to attach | + +**Examples:** +```bash +tigris access-keys attach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy +tigris access-keys attach-policy tid_AaBb +``` + +#### `tigris access-keys detach-policy` (dp) + +Detach an IAM policy from an access key. If no policy ARN is provided, shows interactive selection of attached policies + +``` +tigris access-keys detach-policy [flags] +``` + +| Flag | Description | +|------|-------------| +| `--policy-arn` | ARN of the policy to detach | +| `--force` | Skip confirmation prompts (alias for --yes) | + +**Examples:** +```bash +tigris access-keys detach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy --yes +tigris access-keys detach-policy tid_AaBb +``` + +#### `tigris access-keys list-policies` (lp) + +List all IAM policies attached to an access key + +``` +tigris access-keys list-policies [flags] +``` + +| Flag | Description | +|------|-------------| +| `--format` | Output format (default: table) | +| `--limit` | Maximum number of items to return per page | +| `-pt, --page-token` | Pagination token from a previous request to fetch the next page | + +**Examples:** +```bash +tigris access-keys list-policies tid_AaBbCcDdEeFf ``` -### `iam` +### `tigris iam` Identity and Access Management - manage policies, users, and permissions | Command | Description | |---------|-------------| -| `iam policies` (p) | Manage IAM policies. Policies define permissions for access keys | -| `iam users` (u) | Manage organization users and invitations | +| `tigris iam policies` (p) | Manage IAM policies. Policies define permissions for access keys | +| `tigris iam users` (u) | Manage organization users and invitations | -#### `iam policies` | `p` +#### `tigris iam policies` (p) Manage IAM policies. Policies define permissions for access keys | Command | Description | |---------|-------------| -| `iam policies list` (l) | List all policies in the current organization | -| `iam policies get` (g) | Show details for a policy including its document and attached users. If no ARN provided, shows interactive selection | -| `iam policies create` (c) | Create a new policy with the given name and policy document. Document can be provided via file, inline JSON, or stdin | -| `iam policies edit` (e) | Update an existing policy's document. Document can be provided via file, inline JSON, or stdin. If no ARN provided, shows interactive selection | -| `iam policies delete` (d) | Delete a policy. If no ARN provided, shows interactive selection | -| `iam policies link-key` (lnk) | Link an access key to a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of unlinked keys | -| `iam policies unlink-key` (ulnk) | Unlink an access key from a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of linked keys | -| `iam policies list-keys` (lk) | List all access keys attached to a policy. If no policy ARN is provided, shows interactive selection | +| `tigris iam policies list` (l) | List all policies in the current organization | +| `tigris iam policies get` (g) | Show details for a policy including its document and attached users. If no ARN provided, shows interactive selection | +| `tigris iam policies create` (c) | Create a new policy with the given name and policy document. Document can be provided via file, inline JSON, or stdin | +| `tigris iam policies edit` (e) | Update an existing policy's document. Document can be provided via file, inline JSON, or stdin. If no ARN provided, shows interactive selection | +| `tigris iam policies delete` (d) | Delete a policy. If no ARN provided, shows interactive selection | +| `tigris iam policies link-key` (lnk) | Link an access key to a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of unlinked keys | +| `tigris iam policies unlink-key` (ulnk) | Unlink an access key from a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of linked keys | +| `tigris iam policies list-keys` (lk) | List all access keys attached to a policy. If no policy ARN is provided, shows interactive selection | -##### `iam policies list` +##### `tigris iam policies list` (l) + +List all policies in the current organization ``` tigris iam policies list [flags] @@ -1101,7 +1210,9 @@ tigris iam policies list [flags] tigris iam policies list ``` -##### `iam policies get` +##### `tigris iam policies get` (g) + +Show details for a policy including its document and attached users. If no ARN provided, shows interactive selection ``` tigris iam policies get [resource] [flags] @@ -1117,7 +1228,9 @@ tigris iam policies get tigris iam policies get arn:aws:iam::org_id:policy/my-policy ``` -##### `iam policies create` +##### `tigris iam policies create` (c) + +Create a new policy with the given name and policy document. Document can be provided via file, inline JSON, or stdin ``` tigris iam policies create [flags] @@ -1135,7 +1248,9 @@ tigris iam policies create my-policy --document '{"Version":"2012-10-17","Statem cat policy.json | tigris iam policies create my-policy ``` -##### `iam policies edit` +##### `tigris iam policies edit` (e) + +Update an existing policy's document. Document can be provided via file, inline JSON, or stdin. If no ARN provided, shows interactive selection ``` tigris iam policies edit [resource] [flags] @@ -1153,7 +1268,9 @@ tigris iam policies edit arn:aws:iam::org_id:policy/my-policy --document policy. cat policy.json | tigris iam policies edit arn:aws:iam::org_id:policy/my-policy ``` -##### `iam policies delete` +##### `tigris iam policies delete` (d) + +Delete a policy. If no ARN provided, shows interactive selection ``` tigris iam policies delete [resource] [flags] @@ -1169,7 +1286,9 @@ tigris iam policies delete tigris iam policies delete arn:aws:iam::org_id:policy/my-policy --yes ``` -##### `iam policies link-key` +##### `tigris iam policies link-key` (lnk) + +Link an access key to a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of unlinked keys ``` tigris iam policies link-key [resource] [flags] @@ -1185,7 +1304,9 @@ tigris iam policies link-key arn:aws:iam::org_id:policy/my-policy --id tid_AaBb tigris iam policies link-key ``` -##### `iam policies unlink-key` +##### `tigris iam policies unlink-key` (ulnk) + +Unlink an access key from a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of linked keys ``` tigris iam policies unlink-key [resource] [flags] @@ -1202,7 +1323,9 @@ tigris iam policies unlink-key arn:aws:iam::org_id:policy/my-policy --id tid_AaB tigris iam policies unlink-key ``` -##### `iam policies list-keys` +##### `tigris iam policies list-keys` (lk) + +List all access keys attached to a policy. If no policy ARN is provided, shows interactive selection ``` tigris iam policies list-keys [resource] [flags] @@ -1218,19 +1341,21 @@ tigris iam policies list-keys arn:aws:iam::org_id:policy/my-policy tigris iam policies list-keys ``` -#### `iam users` | `u` +#### `tigris iam users` (u) Manage organization users and invitations | Command | Description | |---------|-------------| -| `iam users list` (l) | List all users and pending invitations in the organization | -| `iam users invite` (i) | Invite users to the organization by email | -| `iam users revoke-invitation` (ri) | Revoke pending invitations. If no invitation ID provided, shows interactive selection | -| `iam users update-role` (ur) | Update user roles in the organization. If no user ID provided, shows interactive selection | -| `iam users remove` (rm) | Remove users from the organization. If no user ID provided, shows interactive selection | +| `tigris iam users list` (l) | List all users and pending invitations in the organization | +| `tigris iam users invite` (i) | Invite users to the organization by email | +| `tigris iam users revoke-invitation` (ri) | Revoke pending invitations. If no invitation ID provided, shows interactive selection | +| `tigris iam users update-role` (ur) | Update user roles in the organization. If no user ID provided, shows interactive selection | +| `tigris iam users remove` (rm) | Remove users from the organization. If no user ID provided, shows interactive selection | -##### `iam users list` +##### `tigris iam users list` (l) + +List all users and pending invitations in the organization ``` tigris iam users list [flags] @@ -1246,7 +1371,9 @@ tigris iam users list tigris iam users list --format json ``` -##### `iam users invite` +##### `tigris iam users invite` (i) + +Invite users to the organization by email ``` tigris iam users invite [flags] @@ -1263,7 +1390,9 @@ tigris iam users invite user@example.com --role admin tigris iam users invite user1@example.com,user2@example.com ``` -##### `iam users revoke-invitation` +##### `tigris iam users revoke-invitation` (ri) + +Revoke pending invitations. If no invitation ID provided, shows interactive selection ``` tigris iam users revoke-invitation [resource] [flags] @@ -1280,7 +1409,9 @@ tigris iam users revoke-invitation invitation_id --yes tigris iam users revoke-invitation id1,id2,id3 --yes ``` -##### `iam users update-role` +##### `tigris iam users update-role` (ur) + +Update user roles in the organization. If no user ID provided, shows interactive selection ``` tigris iam users update-role [resource] [flags] @@ -1298,7 +1429,9 @@ tigris iam users update-role id1,id2 --role admin tigris iam users update-role id1,id2 --role admin,member ``` -##### `iam users remove` +##### `tigris iam users remove` (rm) + +Remove users from the organization. If no user ID provided, shows interactive selection ``` tigris iam users remove [resource] [flags] @@ -1315,21 +1448,6 @@ tigris iam users remove user@example.com --yes tigris iam users remove user@example.com,user@example.net --yes ``` -## Other - -### `update` - -Update the CLI to the latest version - -``` -tigris update -``` - -**Examples:** -```bash -tigris update -``` - ## License MIT diff --git a/package-lock.json b/package-lock.json index 2c9d7d9..1f95271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@aws-sdk/credential-providers": "^3.1035.0", + "@aws-sdk/credential-providers": "^3.1038.0", "@smithy/shared-ini-file-loader": "^4.4.9", - "@tigrisdata/iam": "^2.1.0", - "@tigrisdata/storage": "^3.2.0", + "@tigrisdata/iam": "^2.1.1", + "@tigrisdata/storage": "^3.6.0", "commander": "^14.0.3", "enquirer": "^2.4.1", - "jose": "^6.2.2", + "jose": "^6.2.3", "open": "^11.0.0", "yaml": "^2.8.3" }, @@ -25,10 +25,11 @@ "tigris": "dist/cli.js" }, "devDependencies": { - "@commitlint/cli": "^20.5.0", + "@commitlint/cli": "^20.5.2", "@commitlint/config-conventional": "^20.5.0", "@eslint/js": "^10.0.1", "@types/node": "^22.19.11", + "conventional-changelog-conventionalcommits": "^9.3.1", "dotenv": "^17.4.2", "eslint": "^10.2.1", "eslint-plugin-simple-import-sort": "^13.0.0", @@ -39,7 +40,7 @@ "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.59.0", + "typescript-eslint": "^8.59.1", "vitest": "^4.1.5" } }, @@ -295,48 +296,48 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1035.0.tgz", - "integrity": "sha512-sHjCtR5GdKVXZ/bEXwqUMoGzak9fnI4Ny/NyG7+gAkYC1Z+CIg5Kr4QSrqGjA+N2iwCly8f2/rGAsNo+JGeaNA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1038.0.tgz", + "integrity": "sha512-tTSXUZXzydM0VUoxcrM4YrhhQfFgepfpbRLEq460650rFAC8NsGhGQ6Ixo7UPV6TKEyI/jQcCnQVi4RVM4SkAg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/credential-provider-node": "^3.972.35", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.34", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.20", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.31", - "@smithy/middleware-retry": "^4.5.4", - "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.0", + "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.48", - "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -345,34 +346,34 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1035.0.tgz", - "integrity": "sha512-Bh1h96CjHMpxg6Rn2G4EE30YiiBh9w/7WmSZIfwLB0X/6lblaJcHggcryrq2uNN2Bx1/CNErMjTpGQzqhA7Rhg==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1044.0.tgz", + "integrity": "sha512-yT3g0Oi0b+pJBJswNxRwWLLBoExQhRx9Iz2rUy1xV0slMogTQN+DSjChI95XTDtpGEcY0qnIK6UYX0XCYdhOKg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/credential-provider-node": "^3.972.35", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", "@aws-sdk/middleware-expect-continue": "^3.972.10", - "@aws-sdk/middleware-flexible-checksums": "^3.974.12", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-location-constraint": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-sdk-s3": "^3.972.33", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/middleware-ssec": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.34", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.21", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.20", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", @@ -383,27 +384,27 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.31", - "@smithy/middleware-retry": "^4.5.4", - "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.0", + "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.48", - "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -411,23 +412,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.4.tgz", - "integrity": "sha512-EbVgyzQ83/Lf6oh1O4vYY47tuYw3Aosthh865LNU77KyotKz+uvEBNmsl/bSVS/vG+IU39mCqcOHrnhmhF4lug==", + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.18", - "@smithy/core": "^3.23.16", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -449,12 +450,12 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.27.tgz", - "integrity": "sha512-ibA1vjaLhS1c/wNa9SSSOunjz5I72DBfAYrtM4G4sKQ3wPuk8ycfvU3AF8nEJBfefqPu2RhHxW7bEC9wiA/MTA==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.29.tgz", + "integrity": "sha512-fklwtMw+9+1TRNa7KOCaaE9P9ubN6PdKCVlviX/vPRNtnMGIivAFrWcYsAcyw+sHPPioiSCSOHKKAhtOkO6IGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "^3.997.2", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -465,12 +466,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.30.tgz", - "integrity": "sha512-dHpeqa29a0cBYq/h59IC2EK3AphLY96nKy4F35kBtiz9GuKDc32UYRTgjZaF8uuJCnqgw9omUZKR+9myyDHC2A==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -481,20 +482,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.32", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.32.tgz", - "integrity": "sha512-A+ZTT//Mswkf9DFEM6XlngwOtYdD8X4CUcoZ2wdpgI8cCs9mcGeuhgTwbGJvealub/MeONOaUr3FbRPMKmTDjg==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.0", + "@smithy/node-http-handler": "^4.6.1", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -502,19 +503,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.34.tgz", - "integrity": "sha512-MoRc7tLnx3JpFkV2R826enEfBUVN8o9Cc7y3hnbMwiWzL/VJhgfxRQzHkEL9vWorMWP7tibltsRcLoid9fsVdw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/credential-provider-env": "^3.972.30", - "@aws-sdk/credential-provider-http": "^3.972.32", - "@aws-sdk/credential-provider-login": "^3.972.34", - "@aws-sdk/credential-provider-process": "^3.972.30", - "@aws-sdk/credential-provider-sso": "^3.972.34", - "@aws-sdk/credential-provider-web-identity": "^3.972.34", - "@aws-sdk/nested-clients": "^3.997.2", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -527,13 +528,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.34.tgz", - "integrity": "sha512-XVSklkRRQ/CQDmv3VVFdZRl5hTFgncFhZrLyi0Ai4LZk5o3jpY5HIfuTK7ad7tixPKa+iQmL9+vg9qNyYZB+nw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/nested-clients": "^3.997.2", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", @@ -546,17 +547,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.35.tgz", - "integrity": "sha512-nVrY7AdGfzYgAa/jd9m06p3ES7QQDaB7zN9c+vXnVXxBRkAs9MjRDPB5AKogWuC6phddltfvHGFqLDJmyU9u/A==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.30", - "@aws-sdk/credential-provider-http": "^3.972.32", - "@aws-sdk/credential-provider-ini": "^3.972.34", - "@aws-sdk/credential-provider-process": "^3.972.30", - "@aws-sdk/credential-provider-sso": "^3.972.34", - "@aws-sdk/credential-provider-web-identity": "^3.972.34", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -569,12 +570,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.30.tgz", - "integrity": "sha512-McJPomNTSEo+C6UA3Zq6pFrcyTUaVsoPPBOvbOHAoIFPc8Z2CMLndqFJOnB+9bVFiBTWQLutlVGmrocBbvv4MQ==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -586,14 +587,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.34.tgz", - "integrity": "sha512-WngYb2K+/yhkDOmDfAOjoCa9Ja3he0DZiAraboKwgWoVRkajDIcDYBCVbUTxtTUldvQoe7VvHLTrBNxvftN1aQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/nested-clients": "^3.997.2", - "@aws-sdk/token-providers": "3.1035.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -605,13 +606,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.34.tgz", - "integrity": "sha512-5KLUH+XmSNRj6amJiJSrPsCxU5l/PYDfxyqPa1MxWhHoQC3sxvGPrSib3IE+HQlfRA4e2kO0bnJy7HJdjvpuuA==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/nested-clients": "^3.997.2", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -623,26 +624,26 @@ } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1035.0.tgz", - "integrity": "sha512-HimZ+jVYJzeD6+pwXvhKX2mvx2fScLbjC4+oz1HF9Vuls/3lAWKHssLLVpCIuXL8Ov6cWe1vQIbwpFajuTAmEA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.1035.0", - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.27", - "@aws-sdk/credential-provider-env": "^3.972.30", - "@aws-sdk/credential-provider-http": "^3.972.32", - "@aws-sdk/credential-provider-ini": "^3.972.34", - "@aws-sdk/credential-provider-login": "^3.972.34", - "@aws-sdk/credential-provider-node": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.30", - "@aws-sdk/credential-provider-sso": "^3.972.34", - "@aws-sdk/credential-provider-web-identity": "^3.972.34", - "@aws-sdk/nested-clients": "^3.997.2", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1038.0.tgz", + "integrity": "sha512-+B9BuRVPPKF0Q6msVS4vUGOsL4eUg7XYogikp56rUEQVoUVxn5ONyWlnNzsDMTv+BwuBgFo5N7gRZtEToAnSgg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1038.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.29", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", @@ -654,14 +655,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1035.0.tgz", - "integrity": "sha512-VkC0kDql0qkv+h6nIZOAxmek3VMCNGsq2v74QT9Zf/WbPlyM5PqyGp1dsPxNSv2iMtoWXzHvqzkZ55ZiJCgq4Q==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1044.0.tgz", + "integrity": "sha512-VMyTkaF87RwDmrNPMmfxRADc4SIU0P85q/WzMpr+6e8MfLzHA/lUSkndM4FLEcEBh/AYUUqbBPHxs+WT6xIHLA==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-endpoint": "^4.4.32", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "buffer": "5.6.0", "events": "3.3.0", @@ -672,7 +673,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1035.0" + "@aws-sdk/client-s3": "^3.1044.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -709,15 +710,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.12.tgz", - "integrity": "sha512-v7n0//P95g+UnmyjCpJkDJFB+EP/9Wx/fQJC5BEiK9Y7VHgmhh6RNPVbqDYz9gsz8mXnxzyYt3tCEVJ1kzo01w==", + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", @@ -725,7 +726,7 @@ "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -793,23 +794,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.33.tgz", - "integrity": "sha512-n8Eh/+kq3u/EodLr8n6sQupu03QGjf122RHXCTGLaHSkavz/2beSKpRlq2oDgfmJZNkAkWF113xbyaUmyOd+YA==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -832,18 +833,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.34.tgz", - "integrity": "sha512-jrmJHyYlTQocR7H4VhvSFhaoedMb2rmlOTvFWD6tNBQ/EVQhTsrNfQUYFuPiOc2wUGxbm5LgCHtnvVmCPgODHw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.3", + "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" }, "engines": { @@ -851,48 +852,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.2.tgz", - "integrity": "sha512-uGGQO08YetrqfInOKG5atRMrCDRQWRuZ9gGfKY6svPmuE4K7ac+XcbCkpWpjcA7yCYsBaKB/Nly4XKgPXUO1PA==", + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.4", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.34", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.21", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.20", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.31", - "@smithy/middleware-retry": "^4.5.4", - "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.0", + "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.48", - "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -917,17 +918,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1035.0.tgz", - "integrity": "sha512-zT+ulZy7/4mqSNL0toB5GuJIBm3nbeGyq/sHPOxIKR3g0bVi5CZupxGvt78yzQeBcXVNZz+orXvaw5ejQ0FGPw==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1044.0.tgz", + "integrity": "sha512-ix8UtiNC5g1wv3TIcgTnvWdugyw8dSsBGwZZzVVoGyYjZH9UJLqiOyvVu6apptlPBeE6aV6Fabsx0b1xYFd2ZA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.21", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", - "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-endpoint": "^4.4.32", "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -936,12 +937,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.21.tgz", - "integrity": "sha512-3EpT+C0QdmTMB5aVeJ5odWSLt9vg2oGzUXl1xvUazKGlkr9OBYnegNWqhhjGgZdv8RmSi5eS8nqqB+euNP2aqA==", + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.33", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", @@ -953,13 +954,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1035.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1035.0.tgz", - "integrity": "sha512-E6IO3Cn+OzBe6Sb5pnubd5Y8qSUMAsVKkD5QSwFfIx5fV1g5SkYwUDRDyPlm90RuIVcCo28wpMJU6W8wXH46Aw==", + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.4", - "@aws-sdk/nested-clients": "^3.997.2", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1051,12 +1052,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.20.tgz", - "integrity": "sha512-owEqyKr0z5hWwk+uHwudwNhyFMZ9f9eSWr/k/XD6yeDCI7hHyc56s4UOY1iBQmoramTbdAY4UCuLLEuKmjVXrg==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.34", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", @@ -1076,13 +1077,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz", - "integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { + "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -1135,15 +1137,15 @@ } }, "node_modules/@commitlint/cli": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", - "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", + "version": "20.5.2", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.2.tgz", + "integrity": "sha512-IXr5xd3IX8SEG936P8gcpozRplkDeDSwJlt8UvoY1winwIy2udTbQ/cOCgbaaxcjdDqVoS29VUcz/wkwnSozbA==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/format": "^20.5.0", "@commitlint/lint": "^20.5.0", - "@commitlint/load": "^20.5.0", + "@commitlint/load": "^20.5.2", "@commitlint/read": "^20.5.0", "@commitlint/types": "^20.5.0", "tinyexec": "^1.0.0", @@ -1257,15 +1259,15 @@ } }, "node_modules/@commitlint/load": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", - "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", + "version": "20.5.2", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.2.tgz", + "integrity": "sha512-zmr0RGDz7vThxW1I8ohb9yBjnGuH9mqwJpn21hInjGla+IlLOkS9ey0+dD5HlkzFlY0lX2NYdA2lDW6/0rO7Gw==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.5.0", + "@commitlint/resolve-extends": "^20.5.2", "@commitlint/types": "^20.5.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", @@ -1320,15 +1322,15 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", - "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", + "version": "20.5.2", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.2.tgz", + "integrity": "sha512-8EhSCU9eNos/5cI1yg64GW79UH1c64O69AfStCsj4zqy6An/qIphVEXj4/+2M6056T8coz00f+UXFn4WUUP1HQ==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/types": "^20.5.0", - "global-directory": "^4.0.1", + "global-directory": "^5.0.0", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" @@ -2131,6 +2133,18 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -3325,9 +3339,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.16", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.16.tgz", - "integrity": "sha512-JStomOrINQA1VqNEopLsgcdgwd42au7mykKqVr30XFw89wLt9sDxJDi4djVPRwQmmzyTGy/uOvTc2ultMpFi1w==", + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -3336,7 +3350,7 @@ "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -3545,13 +3559,13 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.31", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.31.tgz", - "integrity": "sha512-KJPdCIN2kOE2aGmqZd7eUTr4WQwOGgtLWgUkswGJggs7rBcQYQjcZMEDa3C0DwbOiXS9L8/wDoQHkfxBYLfiLw==", + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.16", - "@smithy/middleware-serde": "^4.2.19", + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", @@ -3564,19 +3578,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.4.tgz", - "integrity": "sha512-/z7nIFK+ZRW3Ie/l3NEVGdy34LvmEOzBrtBAvgWZ/4PrKX0xP3kWm8pkfcwUk523SqxZhdbQP9JSXgjF77Uhpw==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.0", - "@smithy/smithy-client": "^4.12.12", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -3585,12 +3599,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.19.tgz", - "integrity": "sha512-Q6y+W9h3iYVMCKWDoVge+OC1LKFqbEKaq8SIWG2X2bWJRpd/6dDLyICcNLT6PbjH3Rr6bmg/SeDB25XFOFfeEw==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.16", + "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" @@ -3628,9 +3642,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.0.tgz", - "integrity": "sha512-P734cAoTFtuGfWa/R3jgBnGlURt2w9bYEBwQNMKf58sRM9RShirB2mKwLsVP+jlG/wxpCu8abv8NxdUts8tdLA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -3696,9 +3710,9 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.0.tgz", - "integrity": "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1" @@ -3740,17 +3754,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.12.tgz", - "integrity": "sha512-daO7SJn4eM6ArbmrEs+/BTbH7af8AEbSL3OMQdcRvvn8tuUcR5rU2n6DgxIV53aXMS42uwK8NgKKCh5XgqYOPQ==", + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.16", - "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.24", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -3847,13 +3861,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.48", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.48.tgz", - "integrity": "sha512-hxVRVPYaRDWa6YQdse1aWX1qrksmLsvNyGBKdc32q4jFzSjxYVNWfstknAfR228TnzS4tzgswXRuYIbhXBuXFQ==", + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -3862,16 +3876,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.53", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.53.tgz", - "integrity": "sha512-ybgCk+9JdBq8pYC8Y6U5fjyS8e4sboyAShetxPNL0rRBtaVl56GSFAxsolVBIea1tXR4LPIzL8i6xqmcf0+DCQ==", + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.17", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.12", + "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -3919,12 +3933,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.3.tgz", - "integrity": "sha512-idjUvd4M9Jj6rXkhqw4H4reHoweuK4ZxYWyOrEp4N2rOF5VtaOlQGLDQJva/8WanNXk9ScQtsAb7o5UHGvFm4A==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -3933,13 +3947,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.24", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.24.tgz", - "integrity": "sha512-na5vv2mBSDzXewLEEoWGI7LQQkfpmFEomBsmOpzLFjqGctm0iMwXY5lAwesY9pIaErkccW0qzEOUcYP+WKneXg==", + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.0", + "@smithy/node-http-handler": "^4.6.1", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", @@ -3977,9 +3991,9 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", - "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -4009,25 +4023,26 @@ "license": "MIT" }, "node_modules/@tigrisdata/iam": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tigrisdata/iam/-/iam-2.1.0.tgz", - "integrity": "sha512-jOFNjthKgugzEy5JIAKz8pw+bjSg+VEw7RcpbwUrPCf9AFmutojhF4KRptgFK7YYzSidoIzPCjWlDoyM+12I/w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tigrisdata/iam/-/iam-2.1.1.tgz", + "integrity": "sha512-l9mjTnFpWGi+Nzved836qM7R/2Qm47kDmfsiHFTO7++y9I12XokpbXhFACsShhlrrWY2seSKn1wkrM8dpRXCDg==", "license": "MIT", "dependencies": { - "dotenv": "^17.3.1" + "dotenv": "^17.4.2" } }, "node_modules/@tigrisdata/storage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-3.2.0.tgz", - "integrity": "sha512-sDBloJ+LrHA5+Ni3h545BFv+ZLwbxUvi9q1J6aoaFZEJwLX//RS9JFI5P5J3CT+A7rh8DM5LGj59E1iu5vdfqg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-3.6.0.tgz", + "integrity": "sha512-l+FHRAA903MOZ/PFBlz6S73YwlJ1Fs+D5/gTPiI2/NtiyIsqj3eNRGDYB+DszT75NIxKWCRoyNornUM+7qXkVw==", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", - "@aws-sdk/client-s3": "^3.1030.0", - "@aws-sdk/lib-storage": "^3.1030.0", - "@aws-sdk/s3-request-presigner": "^3.1030.0", - "@smithy/signature-v4": "^5.3.13", + "@aws-sdk/client-s3": "^3.1038.0", + "@aws-sdk/lib-storage": "^3.1038.0", + "@aws-sdk/s3-request-presigner": "^3.1038.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/signature-v4": "^5.3.14", "dotenv": "^17.4.2" } }, @@ -4099,17 +4114,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", + "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/type-utils": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -4122,7 +4137,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", + "@typescript-eslint/parser": "^8.59.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -4138,16 +4153,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", + "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3" }, "engines": { @@ -4163,14 +4178,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", + "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", + "@typescript-eslint/tsconfig-utils": "^8.59.1", + "@typescript-eslint/types": "^8.59.1", "debug": "^4.4.3" }, "engines": { @@ -4185,14 +4200,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", + "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4203,9 +4218,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", + "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", "dev": true, "license": "MIT", "engines": { @@ -4220,15 +4235,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", + "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -4245,9 +4260,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", - "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", + "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", "dev": true, "license": "MIT", "engines": { @@ -4259,16 +4274,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", - "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", + "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.0", - "@typescript-eslint/tsconfig-utils": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/project-service": "8.59.1", + "@typescript-eslint/tsconfig-utils": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -4287,16 +4302,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", + "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4311,13 +4326,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", + "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4492,9 +4507,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5129,13 +5144,13 @@ } }, "node_modules/cosmiconfig-typescript-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", - "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", "dev": true, "license": "MIT", "dependencies": { - "jiti": "^2.6.1" + "jiti": "2.6.1" }, "engines": { "node": ">=v18" @@ -5869,9 +5884,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -5886,9 +5901,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -5901,9 +5916,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -5912,9 +5927,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6191,16 +6207,16 @@ } }, "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", "dev": true, "license": "MIT", "dependencies": { - "ini": "4.1.1" + "ini": "6.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6460,13 +6476,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/into-stream": { @@ -6697,9 +6713,9 @@ } }, "node_modules/jose": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", - "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -11458,16 +11474,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", - "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", + "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.0", - "@typescript-eslint/parser": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0" + "@typescript-eslint/eslint-plugin": "8.59.1", + "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 7a720d9..ebd193d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "scripts": { "build": "tsc --noEmit && tsup", "dev": "export $(grep -v '^#' .env.test | xargs) && (tsc --noEmit --watch --preserveWatchOutput & tsup --watch)", - "cli": "node dist/cli.js", + "cli": "export $(grep -v '^#' .env.test | xargs) && node dist/cli.js", "lint": "eslint src test", "lint:fix": "eslint src test --fix", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", @@ -76,7 +76,12 @@ [ "@semantic-release/commit-analyzer", { + "preset": "conventionalcommits", "releaseRules": [ + { + "breaking": true, + "release": "major" + }, { "type": "refactor", "release": "patch" @@ -84,27 +89,33 @@ ] } ], - "@semantic-release/release-notes-generator", + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits" + } + ], "@semantic-release/github", "@semantic-release/npm" ] }, "dependencies": { - "@aws-sdk/credential-providers": "^3.1035.0", + "@aws-sdk/credential-providers": "^3.1038.0", "@smithy/shared-ini-file-loader": "^4.4.9", - "@tigrisdata/iam": "^2.1.0", - "@tigrisdata/storage": "^3.2.0", + "@tigrisdata/iam": "^2.1.1", + "@tigrisdata/storage": "^3.6.0", "commander": "^14.0.3", "enquirer": "^2.4.1", - "jose": "^6.2.2", + "jose": "^6.2.3", "open": "^11.0.0", "yaml": "^2.8.3" }, "devDependencies": { - "@commitlint/cli": "^20.5.0", + "@commitlint/cli": "^20.5.2", "@commitlint/config-conventional": "^20.5.0", "@eslint/js": "^10.0.1", "@types/node": "^22.19.11", + "conventional-changelog-conventionalcommits": "^9.3.1", "dotenv": "^17.4.2", "eslint": "^10.2.1", "eslint-plugin-simple-import-sort": "^13.0.0", @@ -115,7 +126,7 @@ "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.59.0", + "typescript-eslint": "^8.59.1", "vitest": "^4.1.5" } } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7efa111 --- /dev/null +++ b/renovate.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":semanticCommitTypeAll(chore)" + ], + "timezone": "UTC", + "schedule": ["* 0-8 * * 1"], + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "labels": ["dependencies"], + "osvVulnerabilityAlerts": true, + "vulnerabilityAlerts": { + "schedule": ["at any time"] + }, + "packageRules": [ + { + "description": "Bundle all minor and patch updates (npm, GitHub Actions) into a single weekly PR", + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "groupName": "non-major dependencies" + }, + { + "description": "Major dependency updates: bundled, bi-weekly cadence (first and third Monday) with a 14-day staleness floor", + "matchUpdateTypes": ["major"], + "groupName": "major dependencies", + "minimumReleaseAge": "14 days", + "schedule": ["* 0-5 1-7,15-21 * 1"] + }, + { + "description": "TypeScript and @types/node major updates: held 60 days post-release (~every 2 months)", + "matchPackageNames": ["typescript", "@types/node"], + "matchUpdateTypes": ["major"], + "groupName": "typescript and node types (major)", + "minimumReleaseAge": "60 days" + } + ] +} diff --git a/scripts/generate-registry.ts b/scripts/generate-registry.ts index 4535f37..6079363 100644 --- a/scripts/generate-registry.ts +++ b/scripts/generate-registry.ts @@ -13,21 +13,12 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import * as YAML from 'yaml'; +import type { CommandSpec, Specs } from '../src/types.js'; + const ROOT = process.cwd(); const SPECS_PATH = join(ROOT, 'src/specs.yaml'); const OUTPUT_PATH = join(ROOT, 'src/command-registry.ts'); -interface CommandSpec { - name: string; - alias?: string; - commands?: CommandSpec[]; - default?: string; -} - -interface Specs { - commands: CommandSpec[]; -} - interface RegistryEntry { key: string; importName: string; @@ -88,6 +79,10 @@ function collectEntries( const entries: RegistryEntry[] = []; for (const cmd of commands) { + // Removed commands have no implementation file by design — the + // cli-core intercepts them and prints a redirect message. + if (cmd.removed) continue; + const currentPath = [...parentPath, cmd.name]; if (cmd.commands && cmd.commands.length > 0) { diff --git a/scripts/update-docs.ts b/scripts/update-docs.ts index 8b57fe8..8b3f23d 100644 --- a/scripts/update-docs.ts +++ b/scripts/update-docs.ts @@ -16,15 +16,17 @@ interface Specs { commands: CommandSpec[]; } -// Check if a command is implemented (has a corresponding .ts file without underscore prefix) function isImplemented(...parts: string[]): boolean { const base = join(libDir, ...parts); const paths = [base + '.ts', join(base, 'index.ts')]; return paths.some((p) => existsSync(p) && !p.includes('/_')); } -// Check if a command or any of its nested subcommands are implemented -function hasImplementation(cmd: CommandSpec, ...parentParts: string[]): boolean { +function hasImplementation( + cmd: CommandSpec, + ...parentParts: string[] +): boolean { + if (cmd.removed) return false; const parts = [...parentParts, cmd.name]; if (isImplemented(...parts)) return true; if (cmd.commands) { @@ -33,38 +35,65 @@ function hasImplementation(cmd: CommandSpec, ...parentParts: string[]): boolean return false; } -function getCommandUsage(cmd: CommandSpec): string { - if (!cmd.arguments) return `tigris ${cmd.name}`; +function aliasList(cmd: CommandSpec): string[] { + if (!cmd.alias) return []; + return Array.isArray(cmd.alias) ? cmd.alias : [cmd.alias]; +} + +function aliasSuffix(cmd: CommandSpec): string { + const aliases = aliasList(cmd); + return aliases.length ? ` (${aliases.join(', ')})` : ''; +} - const positionals = cmd.arguments - .filter((a) => a.type === 'positional') +function getPositionalSuffix(cmd: CommandSpec): string { + const positionals = (cmd.arguments ?? []) + .filter((a) => a.type === 'positional' && !a.removed) .map((a) => (a.required ? `<${a.name}>` : `[${a.name}]`)); + return positionals.length ? ' ' + positionals.join(' ') : ''; +} - return `tigris ${cmd.name}${positionals.length ? ' ' + positionals.join(' ') : ''}`; +function renderCommandTable( + commands: CommandSpec[], + parentPath: string[] +): string[] { + const lines: string[] = []; + lines.push('| Command | Description |'); + lines.push('|---------|-------------|'); + for (const cmd of commands) { + const fullName = [...parentPath, cmd.name].join(' '); + lines.push( + `| \`tigris ${fullName}\`${aliasSuffix(cmd)} | ${cmd.description ?? ''} |` + ); + } + lines.push(''); + return lines; } -function generateCommandSection(cmd: CommandSpec): string { +function renderLeafDetail(cmd: CommandSpec, parentPath: string[]): string[] { const lines: string[] = []; - const aliasStr = cmd.alias ? ` | \`${cmd.alias}\`` : ''; + const fullName = [...parentPath, cmd.name].join(' '); + const positionals = getPositionalSuffix(cmd); + const flags = (cmd.arguments ?? []).filter( + (a) => a.type !== 'positional' && !a.removed + ); - lines.push(`### \`${cmd.name}\`${aliasStr}`); - lines.push(''); - lines.push(cmd.description); - lines.push(''); lines.push('```'); - const usage = getCommandUsage(cmd); - const hasFlags = cmd.arguments?.some((a) => a.type !== 'positional'); - lines.push(`${usage}${hasFlags ? ' [flags]' : ''}`); + lines.push( + `tigris ${fullName}${positionals}${flags.length ? ' [flags]' : ''}` + ); lines.push('```'); lines.push(''); - const flags = cmd.arguments?.filter((a) => a.type !== 'positional') || []; if (flags.length > 0) { lines.push('| Flag | Description |'); lines.push('|------|-------------|'); for (const arg of flags) { - const flagName = arg.alias ? `-${arg.alias}, --${arg.name}` : `--${arg.name}`; - lines.push(`| \`${flagName}\` | ${arg.description} |`); + const flagName = arg.alias + ? `-${arg.alias}, --${arg.name}` + : `--${arg.name}`; + const defaultStr = + arg.default !== undefined ? ` (default: ${arg.default})` : ''; + lines.push(`| \`${flagName}\` | ${arg.description ?? ''}${defaultStr} |`); } lines.push(''); } @@ -72,145 +101,44 @@ function generateCommandSection(cmd: CommandSpec): string { if (cmd.examples && cmd.examples.length > 0) { lines.push('**Examples:**'); lines.push('```bash'); - for (const ex of cmd.examples) { - lines.push(ex); - } + for (const ex of cmd.examples) lines.push(ex); lines.push('```'); lines.push(''); - } else { - const positionals = cmd.arguments?.filter((a) => a.type === 'positional') || []; - if (positionals.length > 0 && positionals.some((p) => p.examples?.length)) { - lines.push('**Examples:**'); - lines.push('```bash'); - if (positionals.length === 1 && positionals[0].examples) { - for (const ex of positionals[0].examples.slice(0, 3)) { - lines.push(`tigris ${cmd.name} ${ex}`); - } - } else if (positionals.length >= 2) { - if (cmd.name === 'cp' || cmd.name === 'mv') { - lines.push(`tigris ${cmd.name} bucket/file.txt bucket/copy.txt`); - lines.push(`tigris ${cmd.name} bucket/folder/ other-bucket/folder/`); - } - } - lines.push('```'); - lines.push(''); - } } - return lines.join('\n'); + return lines; } -function generateResourceSection( +function renderCommand( cmd: CommandSpec, - parentPath: string[] = [], - headerLevel: string = '###' -): string { - const lines: string[] = []; - const aliasStr = cmd.alias ? ` | \`${cmd.alias}\`` : ''; - const commandPath = [...parentPath, cmd.name]; - const fullName = commandPath.join(' '); - const subHeaderLevel = headerLevel === '###' ? '####' : '#####'; - - lines.push(`${headerLevel} \`${fullName}\`${aliasStr}`); - lines.push(''); - lines.push(cmd.description); - lines.push(''); - - const subcommands = cmd.commands || []; - - // Check if subcommands have their own nested subcommands (e.g. iam -> policies -> list) - const hasNestedSubcommands = subcommands.some((sub) => sub.commands && sub.commands.length > 0); - - if (hasNestedSubcommands) { - // Parent resource (like iam) - recurse into sub-resources - const implementedSubs = subcommands.filter((sub) => hasImplementation(sub, ...commandPath)); - - if (implementedSubs.length > 0) { - lines.push('| Command | Description |'); - lines.push('|---------|-------------|'); - for (const sub of implementedSubs) { - const subAlias = sub.alias - ? ` (${Array.isArray(sub.alias) ? sub.alias[0] : sub.alias})` - : ''; - lines.push(`| \`${fullName} ${sub.name}\`${subAlias} | ${sub.description} |`); - } - lines.push(''); - - for (const sub of implementedSubs) { - lines.push(generateResourceSection(sub, commandPath, subHeaderLevel)); - } - } - } else { - // Leaf resource - show implemented operations - const implementedOps = subcommands.filter((op) => isImplemented(...commandPath, op.name)); - - if (implementedOps.length > 0) { - lines.push('| Command | Description |'); - lines.push('|---------|-------------|'); - for (const op of implementedOps) { - const opAlias = op.alias - ? ` (${Array.isArray(op.alias) ? op.alias[0] : op.alias})` - : ''; - lines.push(`| \`${fullName} ${op.name}\`${opAlias} | ${op.description} |`); - } - lines.push(''); - - for (const op of implementedOps) { - lines.push(generateOperationSection(commandPath, op, subHeaderLevel)); - } - } - } - - return lines.join('\n'); -} - -function generateOperationSection( parentPath: string[], - op: CommandSpec, - headerLevel: string = '####' + level: number ): string { const lines: string[] = []; - const fullName = [...parentPath, op.name].join(' '); + const fullName = [...parentPath, cmd.name].join(' '); + const hash = '#'.repeat(Math.min(level, 6)); - lines.push(`${headerLevel} \`${fullName}\``); + lines.push(`${hash} \`tigris ${fullName}\`${aliasSuffix(cmd)}`); lines.push(''); + if (cmd.description) { + lines.push(cmd.description); + lines.push(''); + } - const positionals = - op.arguments - ?.filter((a) => a.type === 'positional') - .map((a) => (a.required ? `<${a.name}>` : `[${a.name}]`)) || []; - - const hasFlags = op.arguments?.some((a) => a.type !== 'positional'); - - lines.push('```'); - lines.push( - `tigris ${fullName}${positionals.length ? ' ' + positionals.join(' ') : ''}${hasFlags ? ' [flags]' : ''}` + const childPath = [...parentPath, cmd.name]; + const subcommands = (cmd.commands ?? []).filter((sub) => + hasImplementation(sub, ...childPath) ); - lines.push('```'); - lines.push(''); - const flags = op.arguments?.filter((a) => a.type !== 'positional') || []; - if (flags.length > 0) { - lines.push('| Flag | Description |'); - lines.push('|------|-------------|'); - for (const arg of flags) { - const flagName = arg.alias ? `-${arg.alias}, --${arg.name}` : `--${arg.name}`; - const defaultStr = arg.default !== undefined ? ` (default: ${arg.default})` : ''; - lines.push(`| \`${flagName}\` | ${arg.description}${defaultStr} |`); - } - lines.push(''); + if (subcommands.length === 0) { + lines.push(...renderLeafDetail(cmd, parentPath)); + return lines.join('\n'); } - if (op.examples && op.examples.length > 0) { - lines.push('**Examples:**'); - lines.push('```bash'); - for (const ex of op.examples) { - lines.push(ex); - } - lines.push('```'); - lines.push(''); + lines.push(...renderCommandTable(subcommands, childPath)); + for (const sub of subcommands) { + lines.push(renderCommand(sub, childPath, level + 1)); } - return lines.join('\n'); } @@ -223,182 +151,21 @@ function generateDocs(specs: Specs): string { lines.push('tigris [flags]'); lines.push('```'); lines.push(''); - lines.push('Run `tigris help` to see all available commands, or `tigris help` for details on a specific command.'); - lines.push(''); - - // Core commands (Unix-style) - only implemented ones - const coreCommands = ['ls', 'mk', 'touch', 'cp', 'mv', 'rm', 'stat', 'presign', 'bundle'].filter((c) => isImplemented(c)); - lines.push('### Core Commands'); - lines.push(''); - for (const cmdName of coreCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(`- \`${getCommandUsage(cmd)}\` - ${cmd.description}`); - } - } - lines.push(''); - - // Auth commands - check both direct implementation and subcommands - const authCommandNames = ['login', 'logout', 'whoami', 'configure']; - const authCommands = authCommandNames.filter((c) => { - if (isImplemented(c)) return true; - const cmd = specs.commands.find((s) => s.name === c); - return cmd?.commands?.some((op) => isImplemented(c, op.name)); - }); - lines.push('### Authentication'); - lines.push(''); - for (const cmdName of authCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(`- \`tigris ${cmd.name}\` - ${cmd.description}`); - } - } + lines.push( + 'Run `tigris help` to see all available commands, or `tigris help` for details on a specific command.' + ); lines.push(''); - // Other commands (CLI management) - const otherCommands = ['update'].filter((c) => isImplemented(c)); - if (otherCommands.length > 0) { - lines.push('### Other'); - lines.push(''); - for (const cmdName of otherCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(`- \`tigris ${cmd.name}\` - ${cmd.description}`); - } - } - lines.push(''); - } - - // Resource management - const resourceCommands = ['organizations', 'access-keys', 'credentials', 'buckets', 'forks', 'snapshots', 'objects', 'iam']; - const implementedResources = resourceCommands.filter((c) => { - const cmd = specs.commands.find((s) => s.name === c); - if (!cmd) return false; - return hasImplementation(cmd); - }); + const topLevel = specs.commands.filter((c) => hasImplementation(c)); - lines.push('### Resources'); + lines.push('### Commands'); lines.push(''); - for (const cmdName of implementedResources) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(`- \`tigris ${cmd.name}\` - ${cmd.description}`); - } - } - lines.push(''); - + lines.push(...renderCommandTable(topLevel, [])); lines.push('---'); lines.push(''); - // Detailed sections - lines.push('## Core Commands'); - lines.push(''); - for (const cmdName of coreCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(generateCommandSection(cmd)); - } - } - - lines.push('## Authentication'); - lines.push(''); - for (const cmdName of authCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - // Commands with subcommands use resource-style docs - if (cmd.commands?.some((op) => isImplemented(cmdName, op.name))) { - lines.push(generateResourceSection(cmd)); - } else { - lines.push(generateCommandSection(cmd)); - } - } - } - - lines.push('## Resources'); - lines.push(''); - - // Organizations first - if (implementedResources.includes('organizations')) { - const orgsCmd = specs.commands.find((c) => c.name === 'organizations'); - if (orgsCmd) { - lines.push(generateResourceSection(orgsCmd)); - } - } - - // Access Keys - if (implementedResources.includes('access-keys')) { - const accessKeysCmd = specs.commands.find((c) => c.name === 'access-keys'); - if (accessKeysCmd) { - lines.push(generateResourceSection(accessKeysCmd)); - } - } - - // Credentials - if (implementedResources.includes('credentials')) { - const credentialsCmd = specs.commands.find((c) => c.name === 'credentials'); - if (credentialsCmd) { - lines.push(generateResourceSection(credentialsCmd)); - } - } - - // Buckets section (buckets, forks, snapshots) - const bucketRelated = ['buckets', 'forks', 'snapshots'].filter((c) => implementedResources.includes(c)); - if (bucketRelated.length > 0) { - lines.push('### Buckets'); - lines.push(''); - lines.push('Buckets are containers for objects. You can also create forks and snapshots of buckets.'); - lines.push(''); - - for (const cmdName of bucketRelated) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(generateResourceSection(cmd, [], '####')); - } - } - } - - // Objects - if (implementedResources.includes('objects')) { - const objectsCmd = specs.commands.find((c) => c.name === 'objects'); - if (objectsCmd) { - lines.push(generateResourceSection(objectsCmd)); - } - } - - // IAM - if (implementedResources.includes('iam')) { - const iamCmd = specs.commands.find((c) => c.name === 'iam'); - if (iamCmd) { - lines.push(generateResourceSection(iamCmd)); - } - } - - // Other commands - if (otherCommands.length > 0) { - lines.push('## Other'); - lines.push(''); - for (const cmdName of otherCommands) { - const cmd = specs.commands.find((c) => c.name === cmdName); - if (cmd) { - lines.push(generateCommandSection(cmd)); - } - } - } - - // Warn about any specs commands that aren't in any category - const allCategorized = new Set([ - ...coreCommands, - ...authCommandNames, - ...resourceCommands, - ...otherCommands, - ]); - const unhandled = specs.commands - .filter((c) => !allCategorized.has(c.name) && hasImplementation(c)) - .map((c) => c.name); - if (unhandled.length > 0) { - console.warn( - `Warning: the following implemented commands are not in any docs category: ${unhandled.join(', ')}` - ); + for (const cmd of topLevel) { + lines.push(renderCommand(cmd, [], 3)); } return lines.join('\n'); @@ -417,13 +184,15 @@ function updateReadme(docsContent: string): void { } const newReadme = - readmeContent.slice(0, usageStart) + docsContent + '\n' + readmeContent.slice(licenseStart); + readmeContent.slice(0, usageStart) + + docsContent + + '\n' + + readmeContent.slice(licenseStart); writeFileSync(readmePath, newReadme); console.log('README.md updated successfully!'); } -// Main const specsPath = join(__dirname, '..', 'src', 'specs.yaml'); const specsContent = readFileSync(specsPath, 'utf-8'); const specs = yaml.parse(specsContent) as Specs; diff --git a/src/auth/client.ts b/src/auth/client.ts index ad38d89..4f2e6a1 100644 --- a/src/auth/client.ts +++ b/src/auth/client.ts @@ -25,13 +25,13 @@ export interface Auth0Config { export function getAuth0Config(): Auth0Config { const isDev = process.env.TIGRIS_ENV === 'development'; const domain = isDev - ? 'auth-dev.tigris.dev' + ? (process.env.AUTH0_DOMAIN ?? 'auth-storage.tigris.dev') : (process.env.AUTH0_DOMAIN ?? 'auth.storage.tigrisdata.io'); const clientId = isDev - ? 'JdJVYIyw0O1uHi5L5OJH903qaWBgd3gF' + ? (process.env.AUTH0_CLIENT_ID ?? 'JdJVYIyw0O1uHi5L5OJH903qaWBgd3gF') : (process.env.AUTH0_CLIENT_ID ?? 'DMejqeM3CQ4IqTjEcd3oA9eEiT40hn8D'); const audience = isDev - ? 'https://tigris-api-dev' + ? (process.env.AUTH0_AUDIENCE ?? 'https://tigris-api-dev') : (process.env.AUTH0_AUDIENCE ?? 'https://tigris-os-api'); const claimsNamespace = process.env.TIGRIS_CLAIMS_NAMESPACE ?? 'https://tigris'; diff --git a/src/cli-core.ts b/src/cli-core.ts index 5fb585c..d12c83f 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -4,7 +4,7 @@ import { exitWithError } from '@utils/exit.js'; import { printDeprecated } from '@utils/messages.js'; -import { Command as CommanderCommand } from 'commander'; +import { Command as CommanderCommand, Option } from 'commander'; import type { Argument, CommandSpec, Specs } from './types.js'; @@ -164,6 +164,12 @@ export function commandHasAnyImplementation( pathParts: string[], hasImplementation: ImplementationChecker ): boolean { + // Removed commands are still registered so we can intercept and + // redirect users to the replacement instead of "unknown command". + if (command.removed) { + return true; + } + if (hasImplementation(pathParts)) { return true; } @@ -181,6 +187,40 @@ export function commandHasAnyImplementation( return false; } +/** + * Print a redirect message and exit. Used for hard-removed commands + * and arguments. `subject` is the human-readable thing the user invoked + * (e.g. `tigris buckets set-ttl` or `--region`). + */ +function printRemovedAndExit( + subject: string, + replacedBy: string | undefined +): never { + const hint = replacedBy + ? ` Use ${replacedBy} instead.` + : ' See the changelog for migration guidance.'; + console.error(`${subject} was removed in this version.${hint}`); + process.exit(1); +} + +/** + * Inspect parsed options for any argument the spec marks as removed. + * If the user supplied one, print the redirect and exit. + */ +function checkRemovedArguments( + args: Argument[] | undefined, + options: Record +): void { + if (!args) return; + for (const arg of args) { + if (!arg.removed) continue; + const value = getOptionValue(options, arg.name, args); + if (value !== undefined) { + printRemovedAndExit(`--${arg.name}`, arg.replaced_by); + } + } +} + export function showCommandHelp( specs: Specs, command: CommandSpec, @@ -188,15 +228,17 @@ export function showCommandHelp( hasImplementation: ImplementationChecker ) { const fullPath = pathParts.join(' '); - console.log(`\n${specs.name} ${fullPath} - ${command.description}\n`); + console.log(`\n${specs.name} ${fullPath} - ${command.description ?? ''}\n`); if (command.commands && command.commands.length > 0) { - const availableCmds = command.commands.filter((cmd) => - commandHasAnyImplementation( - cmd, - [...pathParts, cmd.name], - hasImplementation - ) + const availableCmds = command.commands.filter( + (cmd) => + !cmd.removed && + commandHasAnyImplementation( + cmd, + [...pathParts, cmd.name], + hasImplementation + ) ); if (availableCmds.length > 0) { @@ -208,14 +250,17 @@ export function showCommandHelp( cmdPart += ` (${aliases.join(', ')})`; } const paddedCmdPart = cmdPart.padEnd(24); - console.log(`${paddedCmdPart}${cmd.description}`); + console.log(`${paddedCmdPart}${cmd.description ?? ''}`); }); console.log(); } } const globalArgs = specs.definitions?.global_arguments ?? []; - const effectiveArgs = getEffectiveArguments(globalArgs, command.arguments); + const effectiveArgs = getEffectiveArguments( + globalArgs, + command.arguments + ).filter((arg) => !arg.removed); if (effectiveArgs.length > 0) { console.log('Arguments:'); effectiveArgs.forEach((arg) => { @@ -248,8 +293,10 @@ export function showMainHelp( console.log('Usage: tigris [command] [options]\n'); console.log('Commands:'); - const availableCommands = specs.commands.filter((cmd) => - commandHasAnyImplementation(cmd, [cmd.name], hasImplementation) + const availableCommands = specs.commands.filter( + (cmd) => + !cmd.removed && + commandHasAnyImplementation(cmd, [cmd.name], hasImplementation) ); availableCommands.forEach((command: CommandSpec) => { @@ -261,7 +308,7 @@ export function showMainHelp( commandPart += ` (${aliases.join(', ')})`; } const paddedCommandPart = commandPart.padEnd(24); - console.log(`${paddedCommandPart}${command.description}`); + console.log(`${paddedCommandPart}${command.description ?? ''}`); }); console.log( `\nUse "${specs.name} help" for more information about a command.` @@ -319,7 +366,15 @@ export function addArgumentsToCommand( arg.required || arg['required-when'] ? ' ' : ' [value]'; } - cmd.option(optionString, arg.description, arg.default); + if (arg.removed) { + // Register but hide from --help so commander still parses the + // value; the dispatch handler intercepts it post-parse. + cmd.addOption( + new Option(optionString, arg.description ?? '').hideHelp() + ); + } else { + cmd.option(optionString, arg.description ?? '', arg.default); + } } }); } @@ -493,13 +548,29 @@ export function registerCommands( continue; } - const cmd = parent.command(spec.name).description(spec.description); + const cmd = parent + .command(spec.name, spec.removed ? { hidden: true } : undefined) + .description(spec.description ?? ''); if (spec.alias) { const aliases = Array.isArray(spec.alias) ? spec.alias : [spec.alias]; aliases.forEach((alias) => cmd.alias(alias)); } + // Removed commands: register a redirect-and-exit action; skip + // children, arguments, and help registration entirely. + if (spec.removed) { + cmd.allowUnknownOption(true); + cmd.allowExcessArguments(true); + cmd.action(() => { + printRemovedAndExit( + `${specs.name} ${currentPath.join(' ')}`, + spec.replaced_by + ); + }); + continue; + } + if (spec.commands && spec.commands.length > 0) { // Has children - recurse registerCommands(config, cmd, spec.commands, currentPath); @@ -524,16 +595,21 @@ export function registerCommands( hasImplementation ); + const extracted = extractArgumentValues( + allArguments, + positionalArgs, + options + ); + if ( allArguments.length > 0 && - !validateRequiredWhen( - allArguments, - extractArgumentValues(allArguments, positionalArgs, options) - ) + !validateRequiredWhen(allArguments, extracted) ) { return; } + checkRemovedArguments(allArguments, extracted); + if (defaultCmd.deprecated && defaultCmd.messages?.onDeprecated) { printDeprecated(defaultCmd.messages.onDeprecated); } @@ -542,7 +618,7 @@ export function registerCommands( loadModule, [...currentPath, defaultCmd.name], positionalArgs, - extractArgumentValues(allArguments, positionalArgs, options) + extracted ); }); } @@ -570,16 +646,21 @@ export function registerCommands( const options = args.pop(); const positionalArgs = args; + const extracted = extractArgumentValues( + spec.arguments || [], + positionalArgs, + options + ); + if ( spec.arguments && - !validateRequiredWhen( - spec.arguments, - extractArgumentValues(spec.arguments, positionalArgs, options) - ) + !validateRequiredWhen(spec.arguments, extracted) ) { return; } + checkRemovedArguments(spec.arguments, extracted); + if (spec.deprecated && spec.messages?.onDeprecated) { printDeprecated(spec.messages.onDeprecated); } @@ -588,7 +669,7 @@ export function registerCommands( loadModule, currentPath, positionalArgs, - extractArgumentValues(spec.arguments || [], positionalArgs, options) + extracted ); }); } diff --git a/src/lib/buckets/create.ts b/src/lib/buckets/create.ts index 5d15266..e53b7c7 100644 --- a/src/lib/buckets/create.ts +++ b/src/lib/buckets/create.ts @@ -33,7 +33,7 @@ export default async function create(options: Record) { 'S', ]); let defaultTier = getOption(options, ['default-tier', 't', 'T']); - let locations = getOption(options, ['locations', 'l', 'L']); + const locations = getOption(options, ['locations', 'l', 'L']); const forkOf = getOption(options, ['fork-of', 'forkOf', 'fork']); const sourceSnapshot = getOption(options, [ 'source-snapshot', @@ -41,27 +41,6 @@ export default async function create(options: Record) { 'source-snap', ]); - // Handle deprecated --region and --consistency options - const deprecatedRegion = getOption(options, ['region', 'r', 'R']); - const deprecatedConsistency = getOption(options, [ - 'consistency', - 'c', - 'C', - ]); - if (deprecatedRegion !== undefined) { - console.warn( - 'Warning: --region is deprecated, use --locations instead. See https://www.tigrisdata.com/docs/buckets/locations/' - ); - if (locations === undefined) { - locations = deprecatedRegion; - } - } - if (deprecatedConsistency !== undefined) { - console.warn( - 'Warning: --consistency is deprecated, use --locations instead. See https://www.tigrisdata.com/docs/buckets/locations/' - ); - } - // Interactive mode: prompt for all values when no name is provided. const interactive = !name; diff --git a/src/lib/buckets/lifecycle/create.ts b/src/lib/buckets/lifecycle/create.ts new file mode 100644 index 0000000..e081df8 --- /dev/null +++ b/src/lib/buckets/lifecycle/create.ts @@ -0,0 +1,87 @@ +import { getStorageConfigWithOrg } from '@auth/provider.js'; +import type { BucketLifecycleRule } from '@tigrisdata/storage'; +import { failWithError } from '@utils/exit.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { + enabledFromInput, + expirationFromInput, + fetchExistingRules, + readRuleInput, + submitRules, + transitionDeltaFromInput, + validateRuleFieldCombinations, +} from './shared.js'; + +const context = msg('buckets lifecycle', 'create'); + +export default async function create(options: Record) { + printStart(context); + + const format = getFormat(options); + const name = getOption(options, ['name']); + + if (!name) { + failWithError(context, 'Bucket name is required'); + } + + const input = readRuleInput(options); + + const validationError = validateRuleFieldCombinations(input); + if (validationError) { + failWithError(context, validationError); + } + + const transition = transitionDeltaFromInput(input); + const expiration = expirationFromInput(input); + + if (!transition.storageClass && !expiration) { + failWithError( + context, + 'A new rule must include a transition (--storage-class with --days or --date) and/or an expiration (--expire-days or --expire-date)' + ); + } + + // A transition requires both a target class and timing. The shared + // validator covers timing-without-class; this check covers the + // inverse (--storage-class without --days/--date) which only applies + // to create. + if ( + transition.storageClass && + input.days === undefined && + input.date === undefined + ) { + failWithError( + context, + '--storage-class requires --days or --date for a new transition rule' + ); + } + + const enabled = enabledFromInput(input); + + const config = await getStorageConfigWithOrg(); + const existing = await fetchExistingRules(context, name, config); + + const newRule: BucketLifecycleRule = { + ...transition, + ...(expiration ? { expiration } : {}), + ...(input.prefix !== undefined ? { filter: { prefix: input.prefix } } : {}), + ...(enabled !== undefined ? { enabled } : {}), + }; + + await submitRules(context, name, [...existing, newRule], config); + + // Re-fetch to find the newly assigned id (server generates UUIDs). + const after = await fetchExistingRules(context, name, config); + const created = after.find((r) => !existing.some((e) => e.id === r.id)); + const createdId = created?.id ?? '(unknown)'; + + if (format === 'json') { + console.log( + JSON.stringify({ action: 'created', bucket: name, id: createdId }) + ); + } + + printSuccess(context, { name, id: createdId }); +} diff --git a/src/lib/buckets/lifecycle/edit.ts b/src/lib/buckets/lifecycle/edit.ts new file mode 100644 index 0000000..7c335af --- /dev/null +++ b/src/lib/buckets/lifecycle/edit.ts @@ -0,0 +1,129 @@ +import { getStorageConfigWithOrg } from '@auth/provider.js'; +import type { BucketLifecycleRule } from '@tigrisdata/storage'; +import { failWithError } from '@utils/exit.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { + enabledFromInput, + expirationFromInput, + fetchExistingRules, + readRuleInput, + submitRules, + transitionDeltaFromInput, + validateRuleFieldCombinations, +} from './shared.js'; + +const context = msg('buckets lifecycle', 'edit'); + +export default async function edit(options: Record) { + printStart(context); + + const format = getFormat(options); + const name = getOption(options, ['name']); + const id = getOption(options, ['id']); + + if (!name) { + failWithError(context, 'Bucket name is required'); + } + if (!id) { + failWithError(context, 'Rule id is required'); + } + + const input = readRuleInput(options); + + // Edit defers the "transition needs a storage class" check until + // after we fetch the target — the existing rule may already supply + // one, in which case `--days 60` alone is valid. + const validationError = validateRuleFieldCombinations(input, { + requireStorageClassForTiming: false, + }); + if (validationError) { + failWithError(context, validationError); + } + + const transition = transitionDeltaFromInput(input); + const expiration = expirationFromInput(input); + const enabled = enabledFromInput(input); + + const hasAnyChange = + transition.storageClass !== undefined || + transition.days !== undefined || + transition.date !== undefined || + expiration !== undefined || + enabled !== undefined || + input.prefix !== undefined; + + if (!hasAnyChange) { + failWithError( + context, + 'Provide at least one field to change (--storage-class, --days, --date, --expire-days, --expire-date, --prefix, --enable, --disable)' + ); + } + + const config = await getStorageConfigWithOrg(); + const existing = await fetchExistingRules(context, name, config); + + const target = existing.find((r) => r.id === id); + if (!target) { + failWithError( + context, + `No lifecycle rule with id "${id}" found. Run \`tigris buckets lifecycle list ${name}\` to see ids.` + ); + } + + // If the user touched any transition field, the merged rule must + // still have both a storage class and timing. Otherwise the API + // accepts the rule but silently drops the transition. + const userTouchedTransition = + input.storageClass !== undefined || + input.days !== undefined || + input.date !== undefined; + + if (userTouchedTransition) { + const finalStorageClass = transition.storageClass ?? target.storageClass; + const finalDays = + input.days !== undefined + ? Number(input.days) + : input.date !== undefined + ? undefined + : target.days; + const finalDate = + input.date !== undefined + ? input.date + : input.days !== undefined + ? undefined + : target.date; + + if (!finalStorageClass) { + failWithError( + context, + '--storage-class is required (this rule has no existing transition target)' + ); + } + if (finalDays === undefined && finalDate === undefined) { + failWithError( + context, + '--days or --date is required (this rule has no existing transition timing)' + ); + } + } + + const updated: BucketLifecycleRule = { + ...target, + ...transition, + ...(expiration ? { expiration } : {}), + ...(input.prefix !== undefined ? { filter: { prefix: input.prefix } } : {}), + ...(enabled !== undefined ? { enabled } : {}), + }; + + const merged = existing.map((r) => (r.id === id ? updated : r)); + + await submitRules(context, name, merged, config); + + if (format === 'json') { + console.log(JSON.stringify({ action: 'updated', bucket: name, id })); + } + + printSuccess(context, { name, id }); +} diff --git a/src/lib/buckets/lifecycle/list.ts b/src/lib/buckets/lifecycle/list.ts new file mode 100644 index 0000000..dd265ab --- /dev/null +++ b/src/lib/buckets/lifecycle/list.ts @@ -0,0 +1,67 @@ +import { getStorageConfig } from '@auth/provider.js'; +import { getBucketInfo } from '@tigrisdata/storage'; +import { failWithError } from '@utils/exit.js'; +import { + formatJson, + formatTable, + formatXml, + type TableColumn, +} from '@utils/format.js'; +import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { formatExpirationCell, formatTransitionCell } from './shared.js'; + +const context = msg('buckets lifecycle', 'list'); + +export default async function list(options: Record) { + printStart(context); + + const name = getOption(options, ['name']); + const format = getFormat(options); + + if (!name) { + failWithError(context, 'Bucket name is required'); + } + + const { data, error } = await getBucketInfo(name, { + config: await getStorageConfig(), + }); + + if (error) { + failWithError(context, error); + } + + const rules = data?.settings.lifecycleRules ?? []; + + if (rules.length === 0) { + printEmpty(context); + return; + } + + const rows = rules.map((rule) => ({ + id: rule.id ?? '-', + prefix: rule.filter?.prefix ?? '-', + transition: formatTransitionCell(rule), + expiration: formatExpirationCell(rule), + status: rule.enabled === false ? 'disabled' : 'enabled', + })); + + const columns: TableColumn[] = [ + { key: 'id', header: 'ID' }, + { key: 'prefix', header: 'Prefix' }, + { key: 'transition', header: 'Transition' }, + { key: 'expiration', header: 'Expiration' }, + { key: 'status', header: 'Status' }, + ]; + + if (format === 'json') { + console.log(formatJson(rows)); + } else if (format === 'xml') { + console.log(formatXml(rows, 'lifecycleRules', 'rule')); + } else { + console.log(formatTable(rows, columns)); + } + + printSuccess(context, { count: rules.length }); +} diff --git a/src/lib/buckets/lifecycle/shared.ts b/src/lib/buckets/lifecycle/shared.ts new file mode 100644 index 0000000..cc1ec97 --- /dev/null +++ b/src/lib/buckets/lifecycle/shared.ts @@ -0,0 +1,219 @@ +import type { TigrisStorageConfig } from '@auth/provider.js'; +import { + type BucketLifecycleRule, + getBucketInfo, + setBucketLifecycle, +} from '@tigrisdata/storage'; +import { describeExpiration, describeTransition } from '@utils/bucket-info.js'; +import { failWithError } from '@utils/exit.js'; +import { type MessageContext } from '@utils/messages.js'; +import { getOption } from '@utils/options.js'; + +const VALID_TRANSITION_CLASSES = [ + 'STANDARD_IA', + 'GLACIER', + 'GLACIER_IR', +] as const; + +type TransitionClass = (typeof VALID_TRANSITION_CLASSES)[number]; + +function isTransitionClass(value: string): value is TransitionClass { + return (VALID_TRANSITION_CLASSES as readonly string[]).includes(value); +} + +function isIsoDate(value: string): boolean { + return /^\d{4}-\d{2}-\d{2}/.test(value) && !isNaN(new Date(value).getTime()); +} + +type RuleInput = { + prefix?: string; + storageClass?: string; + days?: string; + date?: string; + expireDays?: string; + expireDate?: string; + enable?: boolean; + disable?: boolean; +}; + +export function readRuleInput(options: Record): RuleInput { + return { + prefix: getOption(options, ['prefix']), + storageClass: getOption(options, ['storage-class', 'storageClass']), + days: getOption(options, ['days']), + date: getOption(options, ['date']), + expireDays: getOption(options, ['expire-days', 'expireDays']), + expireDate: getOption(options, ['expire-date', 'expireDate']), + enable: getOption(options, ['enable']), + disable: getOption(options, ['disable']), + }; +} + +/** + * Validates field formats and intra-input conflicts (date vs days, + * enable vs disable, ISO format, positive numbers). Does NOT enforce + * "at least one of transition/expiration" or "transition needs both + * class and timing" — those are structural rules the caller decides + * based on create vs edit semantics. + * + * `requireStorageClassForTiming` defaults to `true` (create semantics). + * Edit passes `false` because the existing rule may already supply a + * storage class; the merged-rule check happens post-fetch in edit.ts. + */ +export function validateRuleFieldCombinations( + input: RuleInput, + { + requireStorageClassForTiming = true, + }: { requireStorageClassForTiming?: boolean } = {} +): string | undefined { + if (input.enable && input.disable) { + return 'Cannot use both --enable and --disable'; + } + + if ( + requireStorageClassForTiming && + (input.days !== undefined || input.date !== undefined) && + !input.storageClass + ) { + return '--storage-class is required when setting --days or --date'; + } + + if (input.storageClass && !isTransitionClass(input.storageClass)) { + return `--storage-class must be one of: ${VALID_TRANSITION_CLASSES.join( + ', ' + )} (STANDARD is not a valid transition target)`; + } + + if (input.days !== undefined && input.date !== undefined) { + return 'Cannot specify both --days and --date'; + } + + if ( + input.days !== undefined && + (isNaN(Number(input.days)) || Number(input.days) <= 0) + ) { + return '--days must be a positive number'; + } + + if (input.date !== undefined && !isIsoDate(input.date)) { + return '--date must be a valid ISO-8601 date (e.g. 2026-06-01)'; + } + + if (input.expireDays !== undefined && input.expireDate !== undefined) { + return 'Cannot specify both --expire-days and --expire-date'; + } + + if ( + input.expireDays !== undefined && + (isNaN(Number(input.expireDays)) || Number(input.expireDays) <= 0) + ) { + return '--expire-days must be a positive number'; + } + + if (input.expireDate !== undefined && !isIsoDate(input.expireDate)) { + return '--expire-date must be a valid ISO-8601 date (e.g. 2026-06-01)'; + } + + if (input.prefix !== undefined && input.prefix === '') { + return '--prefix cannot be empty'; + } + + return undefined; +} + +/** + * Build a transition delta to merge into a rule. `days` and `date` are + * mutually exclusive in the API — when the user sets one, this delta + * explicitly nulls the other so spreading over an existing rule + * overrides a previously-set value instead of leaving both populated. + */ +export function transitionDeltaFromInput(input: RuleInput): { + storageClass?: TransitionClass; + days?: number; + date?: string | undefined; +} { + const delta: { + storageClass?: TransitionClass; + days?: number; + date?: string | undefined; + } = {}; + if (input.storageClass) { + delta.storageClass = input.storageClass as TransitionClass; + } + if (input.days !== undefined) { + delta.days = Number(input.days); + delta.date = undefined; + } else if (input.date !== undefined) { + delta.date = input.date; + delta.days = undefined; + } + return delta; +} + +/** + * Build an expiration object from input, or undefined if neither + * --expire-days nor --expire-date was provided. `days` and `date` are + * mutually exclusive — the unset one is emitted as `undefined` so + * spreading over an existing expiration clears the conflicting field. + */ +export function expirationFromInput( + input: RuleInput +): { days?: number; date?: string | undefined } | undefined { + if (input.expireDays !== undefined) { + return { days: Number(input.expireDays), date: undefined }; + } + if (input.expireDate !== undefined) { + return { date: input.expireDate, days: undefined }; + } + return undefined; +} + +/** + * Resolve `--enable` / `--disable` into a boolean, or undefined when + * neither flag was set. + */ +export function enabledFromInput(input: RuleInput): boolean | undefined { + if (input.enable) return true; + if (input.disable) return false; + return undefined; +} + +/** + * Fetch existing lifecycle rules. Existing rules are passed through to + * `submitRules` with their ids so the SDK's auto-match doesn't silently + * overwrite a rule when there's exactly one update + one existing. + */ +export async function fetchExistingRules( + context: MessageContext, + bucket: string, + config: TigrisStorageConfig +): Promise { + const { data, error } = await getBucketInfo(bucket, { config }); + if (error) { + failWithError(context, error); + } + return data?.settings.lifecycleRules ?? []; +} + +export async function submitRules( + context: MessageContext, + bucket: string, + rules: BucketLifecycleRule[], + config: TigrisStorageConfig +): Promise { + const { error } = await setBucketLifecycle(bucket, { + lifecycleRules: rules, + config, + }); + if (error) { + failWithError(context, error); + } +} + +export function formatTransitionCell(rule: BucketLifecycleRule): string { + return describeTransition(rule) ?? '-'; +} + +export function formatExpirationCell(rule: BucketLifecycleRule): string { + return describeExpiration(rule) ?? '-'; +} diff --git a/src/lib/buckets/set-transition.ts b/src/lib/buckets/set-transition.ts deleted file mode 100644 index ff9a3b0..0000000 --- a/src/lib/buckets/set-transition.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { getStorageConfigWithOrg } from '@auth/provider.js'; -import { - type BucketLifecycleRule, - setBucketLifecycle, -} from '@tigrisdata/storage'; -import { failWithError } from '@utils/exit.js'; -import { msg, printStart, printSuccess } from '@utils/messages.js'; -import { getFormat, getOption } from '@utils/options.js'; - -const context = msg('buckets', 'set-transition'); - -const VALID_TRANSITION_CLASSES = ['STANDARD_IA', 'GLACIER', 'GLACIER_IR']; - -export default async function setTransitions(options: Record) { - printStart(context); - - const format = getFormat(options); - - const name = getOption(options, ['name']); - const storageClass = getOption(options, [ - 'storage-class', - 'storageClass', - ]); - const days = getOption(options, ['days']); - const date = getOption(options, ['date']); - const enable = getOption(options, ['enable']); - const disable = getOption(options, ['disable']); - - if (!name) { - failWithError(context, 'Bucket name is required'); - } - - if (enable && disable) { - failWithError(context, 'Cannot use both --enable and --disable'); - } - - if ( - disable && - (days !== undefined || date !== undefined || storageClass !== undefined) - ) { - failWithError( - context, - 'Cannot use --disable with --days, --date, or --storage-class' - ); - } - - if (!enable && !disable && days === undefined && date === undefined) { - failWithError(context, 'Provide --days, --date, --enable, or --disable'); - } - - if ((days !== undefined || date !== undefined) && !storageClass) { - failWithError( - context, - '--storage-class is required when setting --days or --date' - ); - } - - if (storageClass && !VALID_TRANSITION_CLASSES.includes(storageClass)) { - failWithError( - context, - `--storage-class must be one of: ${VALID_TRANSITION_CLASSES.join(', ')} (STANDARD is not a valid transition target)` - ); - } - - if (days !== undefined && (isNaN(Number(days)) || Number(days) <= 0)) { - failWithError(context, '--days must be a positive number'); - } - - if (date !== undefined) { - if ( - typeof date !== 'string' || - !/^\d{4}-\d{2}-\d{2}/.test(date) || - isNaN(new Date(date).getTime()) - ) { - failWithError( - context, - '--date must be a valid ISO-8601 date (e.g. 2026-06-01)' - ); - } - } - - const finalConfig = await getStorageConfigWithOrg(); - - const rule: BucketLifecycleRule = { - ...(enable ? { enabled: true } : {}), - ...(disable ? { enabled: false } : {}), - ...(storageClass - ? { storageClass: storageClass as BucketLifecycleRule['storageClass'] } - : {}), - ...(days !== undefined ? { days: Number(days) } : {}), - ...(date !== undefined ? { date } : {}), - }; - - const { error } = await setBucketLifecycle(name, { - lifecycleRules: [rule], - config: finalConfig, - }); - - if (error) { - failWithError(context, error); - } - - if (format === 'json') { - console.log(JSON.stringify({ action: 'updated', bucket: name })); - } - - printSuccess(context, { name }); -} diff --git a/src/lib/buckets/set-ttl.ts b/src/lib/buckets/set-ttl.ts deleted file mode 100644 index 941bda9..0000000 --- a/src/lib/buckets/set-ttl.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { getStorageConfigWithOrg } from '@auth/provider.js'; -import { setBucketTtl } from '@tigrisdata/storage'; -import { failWithError } from '@utils/exit.js'; -import { msg, printStart, printSuccess } from '@utils/messages.js'; -import { getFormat, getOption } from '@utils/options.js'; - -const context = msg('buckets', 'set-ttl'); - -export default async function setTtl(options: Record) { - printStart(context); - - const format = getFormat(options); - - const name = getOption(options, ['name']); - const days = getOption(options, ['days']); - const date = getOption(options, ['date']); - const enable = getOption(options, ['enable']); - const disable = getOption(options, ['disable']); - - if (!name) { - failWithError(context, 'Bucket name is required'); - } - - if (enable && disable) { - failWithError(context, 'Cannot use both --enable and --disable'); - } - - if (disable && (days !== undefined || date !== undefined)) { - failWithError(context, 'Cannot use --disable with --days or --date'); - } - - if (!enable && !disable && days === undefined && date === undefined) { - failWithError(context, 'Provide --days, --date, --enable, or --disable'); - } - - if (days !== undefined && (isNaN(Number(days)) || Number(days) <= 0)) { - failWithError(context, '--days must be a positive number'); - } - - if (date !== undefined) { - if ( - typeof date !== 'string' || - !/^\d{4}-\d{2}-\d{2}/.test(date) || - isNaN(new Date(date).getTime()) - ) { - failWithError( - context, - '--date must be a valid ISO-8601 date (e.g. 2026-06-01)' - ); - } - } - - const finalConfig = await getStorageConfigWithOrg(); - - const ttlConfig = { - ...(enable ? { enabled: true } : {}), - ...(disable ? { enabled: false } : {}), - ...(days !== undefined ? { days: Number(days) } : {}), - ...(date !== undefined ? { date } : {}), - }; - - const { error } = await setBucketTtl(name, { - ttlConfig, - config: finalConfig, - }); - - if (error) { - failWithError(context, error); - } - - if (format === 'json') { - console.log(JSON.stringify({ action: 'updated', bucket: name })); - } - - printSuccess(context, { name }); -} diff --git a/src/lib/buckets/set.ts b/src/lib/buckets/set.ts index 1dcc632..933cae7 100644 --- a/src/lib/buckets/set.ts +++ b/src/lib/buckets/set.ts @@ -14,18 +14,7 @@ export default async function set(options: Record) { const name = getOption(options, ['name']); const access = getOption(options, ['access']); - let locations = getOption(options, ['locations']); - - // Handle deprecated --region option - const deprecatedRegion = getOption(options, ['region']); - if (deprecatedRegion !== undefined) { - console.warn( - 'Warning: --region is deprecated, use --locations instead. See https://www.tigrisdata.com/docs/buckets/locations/' - ); - if (locations === undefined) { - locations = deprecatedRegion; - } - } + const locations = getOption(options, ['locations']); const allowObjectAcl = getOption(options, [ 'allow-object-acl', 'allowObjectAcl', diff --git a/src/lib/cp.ts b/src/lib/cp.ts index 879bc80..8a30511 100644 --- a/src/lib/cp.ts +++ b/src/lib/cp.ts @@ -1,8 +1,9 @@ import { getStorageConfig } from '@auth/provider.js'; -import { get, head, list, put } from '@tigrisdata/storage'; +import { copy, get, head, list, put } from '@tigrisdata/storage'; import { executeWithConcurrency } from '@utils/concurrency.js'; import { exitWithError } from '@utils/exit.js'; import { formatSize } from '@utils/format.js'; +import { getContentType } from '@utils/mime.js'; import { getFormat, getOption } from '@utils/options.js'; import { globToRegex, @@ -113,8 +114,11 @@ async function uploadFile( const fileStream = createReadStream(localPath); const body = Readable.toWeb(fileStream) as ReadableStream; + const contentType = getContentType(localPath); + const { error: putError } = await put(key, body, { ...calculateUploadParams(fileSize), + ...(contentType ? { contentType } : {}), onUploadProgress: showProgress ? ({ loaded }) => { if (fileSize !== undefined && fileSize > 0) { @@ -206,9 +210,11 @@ async function copyObject( srcBucket: string, srcKey: string, destBucket: string, - destKey: string, - showProgress = false + destKey: string ): Promise<{ error?: string }> { + // Folder markers (zero-byte objects ending in `/`) are still + // created via put('') — CopyObject on a literal folder marker is + // ambiguous, and the marker has no payload to preserve. if (srcKey.endsWith('/')) { const { error: putError } = await put(destKey, '', { config: { @@ -224,54 +230,19 @@ async function copyObject( return {}; } - let fileSize: number | undefined; - if (showProgress) { - const { data: headData } = await head(srcKey, { - config: { - ...config, - bucket: srcBucket, - }, - }); - fileSize = headData?.size; - } - - const { data, error: getError } = await get(srcKey, 'stream', { - config: { - ...config, - bucket: srcBucket, - }, - }); - - if (getError) { - return { error: getError.message }; - } - - const { error: putError } = await put(destKey, data, { - ...calculateUploadParams(fileSize), - onUploadProgress: showProgress - ? ({ loaded }) => { - if (fileSize !== undefined && fileSize > 0) { - const pct = Math.round((loaded / fileSize) * 100); - process.stdout.write( - `\rCopying: ${formatSize(loaded)} / ${formatSize(fileSize)} (${pct}%)` - ); - } else { - process.stdout.write(`\rCopying: ${formatSize(loaded)}`); - } - } - : undefined, + // Server-side CopyObject. No bytes flow through the client and the + // source's Content-Type / metadata are preserved automatically. + const { error: copyError } = await copy(srcKey, destKey, { + srcBucket, + destBucket, config: { ...config, bucket: destBucket, }, }); - if (showProgress) { - process.stdout.write('\r' + ' '.repeat(60) + '\r'); - } - - if (putError) { - return { error: putError.message }; + if (copyError) { + return { error: copyError.message }; } return {}; @@ -819,8 +790,7 @@ async function copyRemoteToRemote( srcParsed.bucket, srcParsed.path, destParsed.bucket, - destKey, - !_jsonMode + destKey ); if (result.error) { diff --git a/src/lib/forks/create.ts b/src/lib/forks/create.ts deleted file mode 100644 index eea4ef6..0000000 --- a/src/lib/forks/create.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getStorageConfig } from '@auth/provider.js'; -import { createBucket } from '@tigrisdata/storage'; -import { failWithError } from '@utils/exit.js'; -import { msg, printStart, printSuccess } from '@utils/messages.js'; -import { getFormat, getOption } from '@utils/options.js'; - -const context = msg('forks', 'create'); - -export default async function create(options: Record) { - printStart(context); - - const format = getFormat(options); - - const name = getOption(options, ['name']); - const forkName = getOption(options, ['fork-name', 'forkName']); - const snapshot = getOption(options, ['snapshot', 's', 'S']); - - if (!name) { - failWithError(context, 'Source bucket name is required'); - } - - if (!forkName) { - failWithError(context, 'Fork name is required'); - } - - const { error } = await createBucket(forkName, { - sourceBucketName: name, - sourceBucketSnapshot: snapshot, - config: await getStorageConfig(), - }); - - if (error) { - failWithError(context, error); - } - - if (format === 'json') { - console.log( - JSON.stringify({ action: 'created', name: forkName, forkOf: name }) - ); - } - - printSuccess(context, { name, forkName }); -} diff --git a/src/lib/forks/list.ts b/src/lib/forks/list.ts deleted file mode 100644 index 62a51cc..0000000 --- a/src/lib/forks/list.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getStorageConfig } from '@auth/provider.js'; -import { getBucketInfo, listBuckets } from '@tigrisdata/storage'; -import { failWithError } from '@utils/exit.js'; -import { formatOutput } from '@utils/format.js'; -import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; -import { getFormat, getOption } from '@utils/options.js'; - -const context = msg('forks', 'list'); - -export default async function list(options: Record) { - printStart(context); - - const name = getOption(options, ['name']); - const format = getFormat(options); - - if (!name) { - failWithError(context, 'Source bucket name is required'); - } - - const config = await getStorageConfig(); - - // First, check if the bucket has forks - const { data: bucketInfo, error: infoError } = await getBucketInfo(name, { - config, - }); - - if (infoError) { - failWithError(context, infoError); - } - - if (!bucketInfo.forkInfo?.hasChildren) { - printEmpty(context); - return; - } - - // List all buckets and filter for forks of the source bucket - const { data, error } = await listBuckets({ config }); - - if (error) { - failWithError(context, error); - } - - // Get info for each bucket to find forks - const forks: Array<{ name: string; created: Date }> = []; - - for (const bucket of data.buckets) { - if (bucket.name === name) continue; - - const { data: info } = await getBucketInfo(bucket.name, { config }); - const isChildOf = info?.forkInfo?.parents?.some( - (p) => p.bucketName === name - ); - if (isChildOf) { - forks.push({ - name: bucket.name, - created: bucket.creationDate, - }); - } - } - - if (forks.length === 0) { - printEmpty(context); - return; - } - - const output = formatOutput(forks, format!, 'forks', 'fork', [ - { key: 'name', header: 'Name' }, - { key: 'created', header: 'Created' }, - ]); - - console.log(output); - printSuccess(context, { count: forks.length }); -} diff --git a/src/lib/mk.ts b/src/lib/mk.ts index 1dc45b4..d698b25 100644 --- a/src/lib/mk.ts +++ b/src/lib/mk.ts @@ -39,7 +39,7 @@ export default async function mk(options: Record) { 't', 'T', ]); - let locations = getOption(options, ['locations', 'l', 'L']); + const locations = getOption(options, ['locations', 'l', 'L']); const forkOf = getOption(options, ['fork-of', 'forkOf', 'fork']); const sourceSnapshot = getOption(options, [ 'source-snapshot', @@ -47,27 +47,6 @@ export default async function mk(options: Record) { 'source-snap', ]); - // Handle deprecated --region and --consistency options - const deprecatedRegion = getOption(options, ['region', 'r', 'R']); - const deprecatedConsistency = getOption(options, [ - 'consistency', - 'c', - 'C', - ]); - if (deprecatedRegion !== undefined) { - console.warn( - 'Warning: --region is deprecated, use --locations instead. See https://www.tigrisdata.com/docs/buckets/locations/' - ); - if (locations === undefined) { - locations = deprecatedRegion; - } - } - if (deprecatedConsistency !== undefined) { - console.warn( - 'Warning: --consistency is deprecated, use --locations instead. See https://www.tigrisdata.com/docs/buckets/locations/' - ); - } - if (sourceSnapshot && !forkOf) { exitWithError('--source-snapshot requires --fork-of'); } diff --git a/src/lib/mv.ts b/src/lib/mv.ts index b597bac..e5ec561 100644 --- a/src/lib/mv.ts +++ b/src/lib/mv.ts @@ -1,7 +1,6 @@ import { getStorageConfig } from '@auth/provider.js'; -import { get, head, list, put, remove } from '@tigrisdata/storage'; +import { copy, list, move, put, remove } from '@tigrisdata/storage'; import { exitWithError } from '@utils/exit.js'; -import { formatSize } from '@utils/format.js'; import { confirm, requireInteractive } from '@utils/interactive.js'; import { getFormat, getOption } from '@utils/options.js'; import { @@ -12,7 +11,6 @@ import { parseRemotePath, wildcardPrefix, } from '@utils/path.js'; -import { calculateUploadParams } from '@utils/upload.js'; let _jsonMode = false; @@ -277,8 +275,7 @@ export default async function mv(options: Record) { srcPath.bucket, srcPath.path, destPath.bucket, - destKey, - !_jsonMode // show progress for single file (not in JSON mode) + destKey ); if (result.error) { @@ -308,12 +305,13 @@ async function moveObject( srcBucket: string, srcKey: string, destBucket: string, - destKey: string, - showProgress = false + destKey: string ): Promise<{ error?: string }> { - // Handle folder markers specially (empty objects ending with /) + // Folder markers (zero-byte objects ending in `/`) are still + // recreated via put('') + remove(). The server's rename header is + // not meaningful for the marker itself and we want to preserve the + // existing semantics here. if (srcKey.endsWith('/')) { - // Put empty string to destination (creates folder marker) const { error: putError } = await put(destKey, '', { config: { ...config, @@ -325,7 +323,6 @@ async function moveObject( return { error: putError.message }; } - // Delete source folder marker const { error: removeError } = await remove(srcKey, { config: { ...config, @@ -342,57 +339,39 @@ async function moveObject( return {}; } - // Get source object size for upload params and progress - const { data: headData } = await head(srcKey, { - config: { - ...config, - bucket: srcBucket, - }, - }); - const fileSize = headData?.size; + // Same-bucket: metadata-only rename via `X-Tigris-Rename: true`. + // One round-trip, no bytes through the client. + if (srcBucket === destBucket) { + const { error: moveError } = await move(srcKey, destKey, { + config: { + ...config, + bucket: srcBucket, + }, + }); - // Get source object - const { data, error: getError } = await get(srcKey, 'stream', { - config: { - ...config, - bucket: srcBucket, - }, - }); + if (moveError) { + return { error: moveError.message }; + } - if (getError) { - return { error: getError.message }; + return {}; } - // Put to destination - const { error: putError } = await put(destKey, data, { - ...calculateUploadParams(fileSize), - onUploadProgress: showProgress - ? ({ loaded }) => { - if (fileSize !== undefined && fileSize > 0) { - const pct = Math.round((loaded / fileSize) * 100); - process.stdout.write( - `\rMoving: ${formatSize(loaded)} / ${formatSize(fileSize)} (${pct}%)` - ); - } else { - process.stdout.write(`\rMoving: ${formatSize(loaded)}`); - } - } - : undefined, + // Cross-bucket: the server doesn't support move across buckets, so + // fall back to server-side CopyObject + DELETE. Still no bytes + // through the client. + const { error: copyError } = await copy(srcKey, destKey, { + srcBucket, + destBucket, config: { ...config, bucket: destBucket, }, }); - if (showProgress) { - process.stdout.write('\r' + ' '.repeat(60) + '\r'); - } - - if (putError) { - return { error: putError.message }; + if (copyError) { + return { error: copyError.message }; } - // Delete source const { error: removeError } = await remove(srcKey, { config: { ...config, diff --git a/src/lib/objects/delete.ts b/src/lib/objects/delete.ts index d4adf19..b197aef 100644 --- a/src/lib/objects/delete.ts +++ b/src/lib/objects/delete.ts @@ -1,5 +1,5 @@ import { getStorageConfig } from '@auth/provider.js'; -import { remove } from '@tigrisdata/storage'; +import { listVersions, remove } from '@tigrisdata/storage'; import { exitWithError, failWithError, @@ -18,6 +18,8 @@ import { resolveObjectArgs } from '@utils/path.js'; const context = msg('objects', 'delete'); +type Target = { key: string; versionId?: string }; + export default async function deleteObject(options: Record) { printStart(context); @@ -26,11 +28,20 @@ export default async function deleteObject(options: Record) { const bucketArg = getOption(options, ['bucket']); const keysArg = getOption(options, ['key']); const force = getOption(options, ['yes', 'y', 'force']); + const versionId = getOption(options, ['version-id', 'versionId']); + const allVersions = !!getOption(options, [ + 'all-versions', + 'allVersions', + ]); if (!bucketArg) { failWithError(context, 'Bucket name or path is required'); } + if (versionId && allVersions) { + failWithError(context, 'Cannot use --version-id with --all-versions'); + } + const resolved = resolveObjectArgs(bucketArg); const bucket = resolved.bucket; const keys = keysArg || resolved.key || undefined; @@ -40,35 +51,122 @@ export default async function deleteObject(options: Record) { } const config = await getStorageConfig(); + const bucketConfig = { ...config, bucket }; const keyList = Array.isArray(keys) ? keys : [keys]; + if (versionId && keyList.length > 1) { + failWithError( + context, + '--version-id targets a single object; pass exactly one key' + ); + } + + // Resolve the list of (key, versionId?) targets to delete. By + // default we issue an unversioned DELETE per key (server creates a + // delete marker on versioned buckets). --version-id hard-deletes + // one specific version. --all-versions enumerates every version + // and every delete marker for each key and hard-deletes them all. + const targets: Target[] = []; + if (allVersions) { + for (const key of keyList) { + let matched = 0; + let keyMarker: string | undefined; + let versionIdMarker: string | undefined; + // listVersions is paginated; walk every page so we don't + // leave older history behind on heavily-versioned keys. + // ListObjectVersions returns entries in (key asc, version-id + // desc) order. Once we see a key that sorts after the target, + // no later page can contain another match — bail out so we + // don't issue thousands of requests just to walk past every + // `a*` key when the user asked for `a`. + let pastTarget = false; + // Drive loop continuation off the explicit `hasMore` flag, + // not marker truthiness. A destructive bulk-delete must never + // silently stop because the server reported more pages but + // omitted a continuation token — bail loudly instead so the + // user doesn't end up with half-deleted history. + for (;;) { + const { data, error } = await listVersions({ + prefix: key, + ...(keyMarker ? { keyMarker } : {}), + ...(versionIdMarker ? { versionIdMarker } : {}), + config: bucketConfig, + }); + if (error) { + failWithError(context, error); + } + // `prefix` is a loose filter — listVersions returns any key + // that starts with `key`. Exact-match before queueing for + // deletion so we don't nuke a sibling like `foo.txt.bak` + // when the user asked for `foo.txt`. + for (const v of data.versions) { + if (v.name === key) { + targets.push({ key, versionId: v.versionId }); + matched++; + } else if (v.name > key) { + pastTarget = true; + } + } + for (const m of data.deleteMarkers) { + if (m.name === key) { + targets.push({ key, versionId: m.versionId }); + matched++; + } else if (m.name > key) { + pastTarget = true; + } + } + if (pastTarget || !data.hasMore) break; + if (!data.nextKeyMarker) { + failWithError( + context, + `listVersions reported more pages but no nextKeyMarker for key '${key}'` + ); + } + keyMarker = data.nextKeyMarker; + versionIdMarker = data.nextVersionIdMarker; + } + + if (matched === 0) { + failWithError( + context, + `No versions or delete markers found for key '${key}'` + ); + } + } + } else if (versionId) { + targets.push({ key: keyList[0], versionId }); + } else { + for (const key of keyList) targets.push({ key }); + } + if (!force) { requireInteractive('Use --yes to skip confirmation'); - const confirmed = await confirm( - `Delete ${keyList.length} object(s) from '${bucket}'?` - ); + const label = allVersions + ? `Hard-delete ${targets.length} version(s) and delete marker(s) for ${keyList.length} object(s) from '${bucket}'?` + : versionId + ? `Hard-delete version '${versionId}' of '${keyList[0]}' from '${bucket}'?` + : `Delete ${keyList.length} object(s) from '${bucket}'?`; + const confirmed = await confirm(label); if (!confirmed) { console.log('Aborted'); return; } } - const deleted: string[] = []; - const errors: { key: string; error: string }[] = []; - for (const key of keyList) { - const { error } = await remove(key, { - config: { - ...config, - bucket, - }, + const deleted: Target[] = []; + const errors: { key: string; versionId?: string; error: string }[] = []; + for (const target of targets) { + const { error } = await remove(target.key, { + ...(target.versionId ? { versionId: target.versionId } : {}), + config: bucketConfig, }); if (error) { - printFailure(context, error.message, { key }); - errors.push({ key, error: error.message }); + printFailure(context, error.message, target); + errors.push({ ...target, error: error.message }); } else { - deleted.push(key); - printSuccess(context, { key }); + deleted.push(target); + printSuccess(context, target); } } @@ -77,7 +175,11 @@ export default async function deleteObject(options: Record) { const jsonOutput: Record = { action: 'deleted', bucket, - keys: deleted, + // `keys` is kept as a flat string[] for backward compatibility + // with consumers that predate versioning support. `deleted` + // carries the richer (key, versionId?) shape for new callers. + keys: deleted.map((d) => d.key), + deleted, errors, }; if (nextActions.length > 0) jsonOutput.nextActions = nextActions; diff --git a/src/lib/objects/get.ts b/src/lib/objects/get.ts index 2cfb982..aac7977 100644 --- a/src/lib/objects/get.ts +++ b/src/lib/objects/get.ts @@ -116,6 +116,7 @@ export default async function getObject(options: Record) { 'snapshotVersion', 'snapshot', ]); + const versionId = getOption(options, ['version-id', 'versionId']); if (!bucketArg) { failWithError(context, 'Bucket name or path is required'); @@ -135,6 +136,7 @@ export default async function getObject(options: Record) { if (mode === 'stream') { const { data, error } = await get(key, 'stream', { ...(snapshotVersion ? { snapshotVersion } : {}), + ...(versionId ? { versionId } : {}), config: { ...config, bucket, @@ -162,6 +164,7 @@ export default async function getObject(options: Record) { } else { const { data, error } = await get(key, 'string', { ...(snapshotVersion ? { snapshotVersion } : {}), + ...(versionId ? { versionId } : {}), config: { ...config, bucket, diff --git a/src/lib/objects/info.ts b/src/lib/objects/info.ts index f14a3a1..334e714 100644 --- a/src/lib/objects/info.ts +++ b/src/lib/objects/info.ts @@ -19,6 +19,7 @@ export default async function objectInfo(options: Record) { 'snapshotVersion', 'snapshot', ]); + const versionId = getOption(options, ['version-id', 'versionId']); if (!bucketArg) { failWithError(context, 'Bucket name or path is required'); @@ -34,6 +35,7 @@ export default async function objectInfo(options: Record) { const { data, error } = await head(key, { ...(snapshotVersion ? { snapshotVersion } : {}), + ...(versionId ? { versionId } : {}), config: { ...config, bucket, diff --git a/src/lib/objects/list-versions.ts b/src/lib/objects/list-versions.ts new file mode 100644 index 0000000..a75b0a9 --- /dev/null +++ b/src/lib/objects/list-versions.ts @@ -0,0 +1,149 @@ +import { getStorageConfig } from '@auth/provider.js'; +import { listVersions } from '@tigrisdata/storage'; +import { failWithError } from '@utils/exit.js'; +import { + formatJson, + formatSize, + formatTable, + formatXml, + type TableColumn, +} from '@utils/format.js'; +import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption, getPaginationOptions } from '@utils/options.js'; +import { parseAnyPath } from '@utils/path.js'; + +const context = msg('objects', 'list-versions'); + +export default async function listObjectVersions( + options: Record +) { + printStart(context); + + const bucketArg = getOption(options, ['bucket']); + const prefixFlag = getOption(options, ['prefix', 'p', 'P']); + const delimiter = getOption(options, ['delimiter', 'd']); + const keyMarker = getOption(options, ['key-marker', 'keyMarker']); + const versionIdMarker = getOption(options, [ + 'version-id-marker', + 'versionIdMarker', + ]); + const format = getFormat(options); + const { limit } = getPaginationOptions(options); + + if (!bucketArg) { + failWithError(context, 'Bucket name is required'); + } + + const parsed = parseAnyPath(bucketArg); + const bucket = parsed.bucket; + const prefix = prefixFlag || parsed.path || undefined; + + const config = await getStorageConfig(); + + const { data, error } = await listVersions({ + prefix, + ...(delimiter ? { delimiter } : {}), + ...(limit !== undefined ? { limit } : {}), + ...(keyMarker ? { keyMarker } : {}), + ...(versionIdMarker ? { versionIdMarker } : {}), + config: { + ...config, + bucket, + }, + }); + + if (error) { + failWithError(context, error); + } + + const versionRows = data.versions.map((v) => ({ + key: v.name, + versionId: v.versionId, + latest: v.isLatest ? 'yes' : '', + size: formatSize(v.size), + modified: v.lastModified, + })); + + const deleteMarkerRows = data.deleteMarkers.map((m) => ({ + key: m.name, + versionId: m.versionId, + latest: m.isLatest ? 'yes' : '', + modified: m.lastModified, + })); + + const versionColumns: TableColumn[] = [ + { key: 'key', header: 'Key' }, + { key: 'versionId', header: 'Version ID' }, + { key: 'latest', header: 'Latest' }, + { key: 'size', header: 'Size' }, + { key: 'modified', header: 'Modified' }, + ]; + + const deleteMarkerColumns: TableColumn[] = [ + { key: 'key', header: 'Key' }, + { key: 'versionId', header: 'Version ID' }, + { key: 'latest', header: 'Latest' }, + { key: 'modified', header: 'Modified' }, + ]; + + // JSON / XML always emit a valid response object — even when both + // arrays are empty — so downstream `jq` / parser consumers don't + // have to special-case the no-results path. The human-readable + // "empty" message only fires in table mode. + if (format === 'json') { + // Mirror the S3 ListObjectVersions response shape so downstream + // `jq` users get the same ergonomics as `aws s3api`. + console.log( + formatJson({ + versions: data.versions, + deleteMarkers: data.deleteMarkers, + commonPrefixes: data.commonPrefixes, + nextKeyMarker: data.nextKeyMarker, + nextVersionIdMarker: data.nextVersionIdMarker, + hasMore: data.hasMore, + }) + ); + } else if (format === 'xml') { + const lines = ['']; + lines.push( + ' ' + + formatXml(versionRows, 'versions', 'version').replace(/\n/g, '\n ') + ); + lines.push( + ' ' + + formatXml(deleteMarkerRows, 'deleteMarkers', 'deleteMarker').replace( + /\n/g, + '\n ' + ) + ); + lines.push(''); + console.log(lines.join('\n')); + } else { + if (versionRows.length === 0 && deleteMarkerRows.length === 0) { + printEmpty(context); + return; + } + if (versionRows.length > 0) { + console.log('\nVersions'); + console.log(formatTable(versionRows, versionColumns)); + } + if (deleteMarkerRows.length > 0) { + console.log('Delete Markers'); + console.log(formatTable(deleteMarkerRows, deleteMarkerColumns)); + } + if (data.hasMore && data.nextKeyMarker) { + // `list-versions` paginates on a (keyMarker, versionIdMarker) + // pair, not the single page-token the generic helper assumes. + let hint = `\nNext page: --key-marker "${data.nextKeyMarker}"`; + if (data.nextVersionIdMarker) { + hint += ` --version-id-marker "${data.nextVersionIdMarker}"`; + } + console.error(hint); + } + } + + printSuccess(context, { + versions: versionRows.length, + deleteMarkers: deleteMarkerRows.length, + }); +} diff --git a/src/lib/objects/put.ts b/src/lib/objects/put.ts index 8ed5cdf..e5616d0 100644 --- a/src/lib/objects/put.ts +++ b/src/lib/objects/put.ts @@ -3,6 +3,7 @@ import { put } from '@tigrisdata/storage'; import { failWithError, printNextActions } from '@utils/exit.js'; import { formatOutput, formatSize } from '@utils/format.js'; import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getContentType } from '@utils/mime.js'; import { getFormat, getOption } from '@utils/options.js'; import { resolveObjectArgs } from '@utils/path.js'; import { calculateUploadParams } from '@utils/upload.js'; @@ -71,9 +72,15 @@ export default async function putObject(options: Record) { ? calculateUploadParams(fileSize) : { multipart: true, partSize: 5 * 1024 * 1024, queueSize: 8 }; + // --content-type wins; otherwise infer from the file extension when + // we have a path. Stdin uploads have no extension to infer from, so + // we leave it unset and let the server default apply. + const resolvedContentType = + contentType ?? (file ? getContentType(file) : undefined); + const { data, error } = await put(key, body, { access: access === 'public' ? 'public' : 'private', - contentType, + contentType: resolvedContentType, ...uploadParams, onUploadProgress: ({ loaded, percentage }) => { if (fileSize !== undefined && fileSize > 0) { diff --git a/src/lib/objects/set-access.ts b/src/lib/objects/set-access.ts new file mode 100644 index 0000000..675d3c7 --- /dev/null +++ b/src/lib/objects/set-access.ts @@ -0,0 +1,62 @@ +import { getStorageConfig } from '@auth/provider.js'; +import { setObjectAccess } from '@tigrisdata/storage'; +import { failWithError } from '@utils/exit.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; +import { resolveObjectArgs } from '@utils/path.js'; + +const context = msg('objects', 'set-access'); + +export default async function setAccess(options: Record) { + printStart(context); + + const format = getFormat(options); + + const bucketArg = getOption(options, ['bucket']); + const keyArg = getOption(options, ['key']); + const accessArg = getOption(options, ['access']); + + if (!bucketArg) { + failWithError(context, 'Bucket name or path is required'); + } + + // When the user passes a full t3://bucket/key path as the first + // positional, the second positional slot is the access value and + // there is no third. Mirrors the resolution shape in objects put. + const combined = resolveObjectArgs(bucketArg); + const bucket = combined.bucket; + const key = combined.key || keyArg; + const access = combined.key ? keyArg : accessArg; + + if (!key) { + failWithError(context, 'Object key is required'); + } + + if (!access) { + failWithError(context, 'Access level is required (public or private)'); + } + + if (access !== 'public' && access !== 'private') { + failWithError(context, 'Access level must be either "public" or "private"'); + } + + const config = await getStorageConfig(); + + const { error } = await setObjectAccess(key, { + access, + config: { + ...config, + bucket, + }, + }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + console.log(JSON.stringify({ action: 'updated', bucket, key, access })); + } + + printSuccess(context, { key, bucket, access }); +} diff --git a/src/lib/objects/set.ts b/src/lib/objects/set.ts index 8443a10..89dda15 100644 --- a/src/lib/objects/set.ts +++ b/src/lib/objects/set.ts @@ -1,5 +1,5 @@ import { getStorageConfig } from '@auth/provider.js'; -import { updateObject } from '@tigrisdata/storage'; +import { move, setObjectAccess } from '@tigrisdata/storage'; import { failWithError } from '@utils/exit.js'; import { msg, printStart, printSuccess } from '@utils/messages.js'; import { getFormat, getOption } from '@utils/options.js'; @@ -35,14 +35,23 @@ export default async function setObject(options: Record) { } const config = await getStorageConfig(); + const finalConfig = { ...config, bucket }; - const { error } = await updateObject(key, { + // Rename first so the access update targets the renamed object. + let currentKey = key; + if (newKey) { + const { error: moveError } = await move(key, newKey, { + config: finalConfig, + }); + if (moveError) { + failWithError(context, moveError); + } + currentKey = newKey; + } + + const { error } = await setObjectAccess(currentKey, { access: access === 'public' ? 'public' : 'private', - ...(newKey && { key: newKey }), - config: { - ...config, - bucket, - }, + config: finalConfig, }); if (error) { @@ -54,12 +63,12 @@ export default async function setObject(options: Record) { JSON.stringify({ action: 'updated', bucket, - key, + key: currentKey, access, ...(newKey ? { newKey } : {}), }) ); } - printSuccess(context, { key, bucket }); + printSuccess(context, { key: currentKey, bucket }); } diff --git a/src/lib/stat.ts b/src/lib/stat.ts index 4483a72..f22a4d5 100644 --- a/src/lib/stat.ts +++ b/src/lib/stat.ts @@ -19,6 +19,7 @@ export default async function stat(options: Record) { 'snapshotVersion', 'snapshot', ]); + const versionId = getOption(options, ['version-id', 'versionId']); const config = await getStorageConfig(); // No path: show overall stats @@ -84,6 +85,7 @@ export default async function stat(options: Record) { // Object path: show object metadata const { data, error } = await head(path, { ...(snapshotVersion ? { snapshotVersion } : {}), + ...(versionId ? { versionId } : {}), config: { ...config, bucket, diff --git a/src/specs.yaml b/src/specs.yaml index 1205e57..02c5b78 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -30,16 +30,6 @@ definitions: value: GLACIER_IR description: Lowest-cost storage for long-lived data that is rarely accessed and requires retrieval in milliseconds. - consistency_options: &consistency_options - - name: Default - value: default - description: Strict read-after-write consistency in same region. Eventual consistency globally. - - name: Strict - value: strict - description: Strict read-after-write consistency globally. Latency will be higher than the default. - - region_options: ®ion_options - location_options: &location_options - name: Global value: 'global' @@ -343,15 +333,13 @@ commands: options: *tier_options default: STANDARD - name: consistency - description: (Deprecated, use --locations) Consistency level (only applies when creating a bucket) alias: c - options: *consistency_options - deprecated: true + removed: true + replaced_by: --locations - name: region - description: (Deprecated, use --locations) Region (only applies when creating a bucket) alias: r - options: *region_options - deprecated: true + removed: true + replaced_by: --locations - name: locations description: Location for the bucket (only applies when creating a bucket) alias: l @@ -408,6 +396,8 @@ commands: - name: snapshot-version description: Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) alias: snapshot + - name: version-id + description: Object version id to stat (requires bucket versioning). Omit to stat the latest version # presign - name: presign @@ -739,15 +729,13 @@ commands: options: *tier_options default: STANDARD - name: consistency - description: (Deprecated, use --locations) Choose the consistency level for the bucket alias: c - options: *consistency_options - deprecated: true + removed: true + replaced_by: --locations - name: region - description: (Deprecated, use --locations) Region alias: r - options: *region_options - deprecated: true + removed: true + replaced_by: --locations - name: locations description: Location for the bucket alias: l @@ -828,10 +816,8 @@ commands: description: Bucket access level options: *access_options - name: region - description: (Deprecated, use --locations) Allowed regions (can specify multiple) - options: *region_options - multiple: true - deprecated: true + removed: true + replaced_by: --locations - name: locations description: Bucket location (see https://www.tigrisdata.com/docs/buckets/locations/ for more details) options: *location_options @@ -854,33 +840,8 @@ commands: type: boolean # set-ttl - name: set-ttl - description: Configure object expiration (TTL) on a bucket. Objects expire after a number of days or on a specific date - examples: - - "tigris buckets set-ttl my-bucket --days 30" - - "tigris buckets set-ttl my-bucket --date 2026-06-01" - - "tigris buckets set-ttl my-bucket --disable" - messages: - onStart: 'Updating TTL settings...' - onSuccess: 'TTL settings updated for bucket {{name}}' - onFailure: 'Failed to update TTL settings' - arguments: - - name: name - description: Name of the bucket - type: positional - required: true - examples: - - my-bucket - - name: days - description: Expire objects after this many days - alias: d - - name: date - description: Expire objects on this date (ISO-8601, e.g. 2026-06-01) - - name: enable - description: Enable TTL on the bucket (uses existing lifecycle rules) - type: flag - - name: disable - description: Disable TTL on the bucket - type: flag + removed: true + replaced_by: 'tigris buckets lifecycle create --expire-days ' # set-locations - name: set-locations description: Set the data locations for a bucket @@ -965,38 +926,122 @@ commands: - t3://my-bucket/prefix/ # set-transition - name: set-transition - description: Configure a lifecycle transition rule on a bucket. Automatically move objects to a different storage class after a number of days or on a specific date + removed: true + replaced_by: 'tigris buckets lifecycle create --storage-class --days ' + # lifecycle + - name: lifecycle + description: Manage bucket lifecycle rules. Each rule combines an optional storage-class transition and/or expiration (TTL), scoped to an optional key prefix + alias: lc examples: - - "tigris buckets set-transition my-bucket --storage-class STANDARD_IA --days 30" - - "tigris buckets set-transition my-bucket --storage-class GLACIER --date 2026-06-01" - - "tigris buckets set-transition my-bucket --enable" - - "tigris buckets set-transition my-bucket --disable" - messages: - onStart: 'Updating lifecycle transition rule...' - onSuccess: 'Lifecycle transition rule updated for bucket {{name}}' - onFailure: 'Failed to update lifecycle transition rule' - arguments: - - name: name - description: Name of the bucket - type: positional - required: true + - "tigris buckets lifecycle list my-bucket" + - "tigris buckets lifecycle create my-bucket --prefix logs/ --storage-class GLACIER --days 90" + - "tigris buckets lifecycle create my-bucket --prefix tmp/ --expire-days 7" + - "tigris buckets lifecycle edit my-bucket --days 30" + commands: + - name: list + description: List lifecycle rules on a bucket + alias: l examples: - - my-bucket - - name: storage-class - description: Target storage class to transition objects to - alias: s - options: *transition_tier_options - - name: days - description: Transition objects after this many days - alias: d - - name: date - description: Transition objects on this date (ISO-8601, e.g. 2026-06-01) - - name: enable - description: Enable lifecycle transition rules on the bucket - type: flag - - name: disable - description: Disable lifecycle transition rules on the bucket - type: flag + - "tigris buckets lifecycle list my-bucket" + - "tigris buckets lifecycle list my-bucket --json" + messages: + onStart: '' + onSuccess: '' + onFailure: 'Failed to list lifecycle rules' + onEmpty: 'No lifecycle rules configured' + arguments: + - name: name + description: Name of the bucket + type: positional + required: true + examples: + - my-bucket + - name: format + description: Output format + options: [json, table, xml] + default: table + - name: create + description: Create a new lifecycle rule. A rule must include a transition (--storage-class with --days or --date) and/or an expiration (--expire-days or --expire-date), and may optionally be scoped via --prefix + alias: c + examples: + - "tigris buckets lifecycle create my-bucket --storage-class STANDARD_IA --days 30" + - "tigris buckets lifecycle create my-bucket --prefix logs/ --storage-class GLACIER --days 90" + - "tigris buckets lifecycle create my-bucket --prefix tmp/ --expire-days 7" + - "tigris buckets lifecycle create my-bucket --prefix archive/ --storage-class GLACIER --days 30 --expire-days 365" + messages: + onStart: 'Creating lifecycle rule...' + onSuccess: 'Lifecycle rule created on bucket {{name}} (id: {{id}})' + onFailure: 'Failed to create lifecycle rule' + arguments: + - name: name + description: Name of the bucket + type: positional + required: true + examples: + - my-bucket + - name: prefix + description: Key prefix to scope the rule to. Omit for a bucket-wide rule + alias: p + - name: storage-class + description: Target storage class for the transition + alias: s + options: *transition_tier_options + - name: days + description: Transition objects after this many days (used with --storage-class) + alias: d + - name: date + description: Transition objects on this date (ISO-8601, e.g. 2026-06-01) (used with --storage-class) + - name: expire-days + description: Expire (delete) objects after this many days + - name: expire-date + description: Expire (delete) objects on this date (ISO-8601, e.g. 2026-06-01) + - name: disable + description: Create the rule in a disabled state + type: flag + - name: edit + description: Edit an existing lifecycle rule by its id. Only specified fields are changed + alias: e + examples: + - "tigris buckets lifecycle edit my-bucket abc123 --days 60" + - "tigris buckets lifecycle edit my-bucket abc123 --expire-days 90" + - "tigris buckets lifecycle edit my-bucket abc123 --enable" + messages: + onStart: 'Updating lifecycle rule...' + onSuccess: 'Lifecycle rule {{id}} updated on bucket {{name}}' + onFailure: 'Failed to update lifecycle rule' + arguments: + - name: name + description: Name of the bucket + type: positional + required: true + examples: + - my-bucket + - name: id + description: Lifecycle rule id (run `tigris buckets lifecycle list` to see ids) + type: positional + required: true + - name: prefix + description: Replace the rule's key prefix + alias: p + - name: storage-class + description: Replace the rule's transition target + alias: s + options: *transition_tier_options + - name: days + description: Replace the rule's transition days + alias: d + - name: date + description: Replace the rule's transition date (ISO-8601) + - name: expire-days + description: Replace the rule's expiration days + - name: expire-date + description: Replace the rule's expiration date (ISO-8601) + - name: enable + description: Enable the rule + type: flag + - name: disable + description: Disable the rule (does not remove it) + type: flag # set-notifications - name: set-notifications description: Configure object event notifications on a bucket. Sends webhook requests to a URL when objects are created, updated, or deleted @@ -1080,69 +1125,12 @@ commands: type: flag ######################### - # Manage forks + # Manage forks (removed) ######################### - name: forks - description: (Deprecated, use "buckets create --fork-of" and "buckets list --forks-of") List and create forks alias: f - examples: - - "tigris forks list my-bucket" - - "tigris forks create my-bucket my-fork" - commands: - # list - - name: list - description: (Deprecated, use "buckets list --forks-of") List all forks created from the given source bucket - deprecated: true - alias: l - examples: - - "tigris forks list my-bucket" - - "tigris forks list my-bucket --format json" - messages: - onStart: 'Listing forks...' - onSuccess: 'Found {{count}} fork(s)' - onFailure: 'Failed to list forks' - onEmpty: 'No forks found for this bucket' - onDeprecated: 'Use "tigris buckets list --forks-of " instead' - arguments: - - name: name - description: Name of the source bucket - type: positional - required: true - examples: - - my-bucket - - name: format - description: Output format - options: [json, table, xml] - default: table - # create - - name: create - description: (Deprecated, use "buckets create --fork-of") Create a new fork (copy-on-write clone) of the source bucket - deprecated: true - alias: c - examples: - - "tigris forks create my-bucket my-fork" - - "tigris forks create my-bucket my-fork --snapshot 1765889000501544464" - messages: - onStart: 'Creating fork...' - onSuccess: "Fork '{{forkName}}' created from '{{name}}'" - onFailure: 'Failed to create fork' - onDeprecated: 'Use "tigris buckets create --fork-of " instead' - arguments: - - name: name - description: Name of the source bucket - type: positional - required: true - examples: - - my-bucket - - name: fork-name - description: Name for the new fork - type: positional - required: true - examples: - - my-fork - - name: snapshot - description: Create fork from a specific snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) - alias: s + removed: true + replaced_by: 'tigris buckets create --fork-of and tigris buckets list --forks-of' ######################### # Manage snapshots @@ -1262,6 +1250,44 @@ commands: - name: source description: List objects from a specific storage source on buckets with shadow migration enabled options: [tigris, shadow] + # list-versions + - name: list-versions + description: List object versions and delete markers in a bucket (requires bucket versioning). Returns both arrays separately to match the S3 ListObjectVersions response + alias: lv + examples: + - "tigris objects list-versions my-bucket" + - "tigris objects list-versions t3://my-bucket/logs/" + - "tigris objects list-versions my-bucket --prefix images/" + - "tigris objects list-versions my-bucket --format json" + messages: + onStart: 'Listing object versions...' + onSuccess: 'Found {{versions}} version(s) and {{deleteMarkers}} delete marker(s)' + onFailure: 'Failed to list object versions' + onEmpty: 'No versions or delete markers found' + arguments: + - name: bucket + description: Name of the bucket, or a path with optional prefix (t3://bucket/prefix/) + type: positional + required: true + examples: + - my-bucket + - t3://my-bucket/images/ + - name: prefix + description: Filter by key prefix + alias: p + - name: delimiter + description: Group keys sharing a common prefix up to the delimiter (e.g. "/" for folder-style grouping) + alias: d + - name: format + description: Output format + options: [json, table, xml] + default: table + - name: limit + description: Maximum number of items to return per page + - name: key-marker + description: Pagination marker — the key to start listing from (from a prior nextKeyMarker) + - name: version-id-marker + description: Pagination marker — the version id to start listing from (from a prior nextVersionIdMarker) # get - name: get description: Download an object by key. Prints to stdout by default, or saves to a file with --output @@ -1297,6 +1323,8 @@ commands: - name: snapshot-version description: Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) alias: snapshot + - name: version-id + description: Object version id to download (requires bucket versioning). Omit to download the latest version # put - name: put description: Upload a local file as an object. Content-type is auto-detected from extension unless overridden @@ -1344,12 +1372,14 @@ commands: default: table # delete - name: delete - description: Delete one or more objects by key from the given bucket + description: Delete one or more objects by key from the given bucket. On a versioned bucket, the default creates a delete marker; use --version-id or --all-versions to hard-delete versions alias: d examples: - "tigris objects delete my-bucket old-file.txt --yes" - "tigris objects delete t3://my-bucket/old-file.txt --yes" - "tigris objects delete my-bucket file-a.txt,file-b.txt --yes" + - "tigris objects delete my-bucket old-file.txt --version-id abc123 --yes" + - "tigris objects delete my-bucket old-file.txt --all-versions --yes" messages: onStart: 'Deleting object...' onSuccess: "Object '{{key}}' deleted successfully" @@ -1371,13 +1401,19 @@ commands: multiple: true examples: - my-file.txt + - name: version-id + description: Hard-delete a specific object version (requires bucket versioning). Targets a single key + - name: all-versions + description: Hard-delete every version and delete marker for the given key(s). Mutually exclusive with --version-id + type: flag - name: force type: flag description: Skip confirmation prompts (alias for --yes) - # set + # set (deprecated) - name: set - description: Update settings on an existing object such as access level + description: (Deprecated) Update settings on an existing object such as access level. Use `tigris objects set-access` for ACL changes and `tigris mv` to rename alias: s + deprecated: true examples: - "tigris objects set my-bucket my-file.txt --access public" - "tigris objects set t3://my-bucket/my-file.txt --access public" @@ -1386,6 +1422,7 @@ commands: onStart: 'Updating object...' onSuccess: "Object '{{key}}' updated successfully" onFailure: 'Failed to update object' + onDeprecated: 'tigris objects set is deprecated. Use `tigris objects set-access` for ACL changes and `tigris mv` to rename objects. This command will be removed in a future release.' arguments: - name: bucket description: Name of the bucket, or a full path (t3://bucket/key) @@ -1402,6 +1439,33 @@ commands: - name: new-key description: Rename the object to a new key alias: n + # set-access + - name: set-access + description: Set the access level (public or private) on an existing object + alias: sa + examples: + - "tigris objects set-access my-bucket my-file.txt public" + - "tigris objects set-access t3://my-bucket/my-file.txt private" + messages: + onStart: 'Updating object access...' + onSuccess: "Access for '{{key}}' updated to {{access}}" + onFailure: 'Failed to update object access' + arguments: + - name: bucket + description: Name of the bucket, or a full path (t3://bucket/key) + type: positional + required: true + - name: key + description: Key of the object (omit if bucket contains the full path) + type: positional + - name: access + description: Access level (public or private) + type: positional + options: *access_options + - name: format + description: Output format + options: [json, table, xml] + default: table # info - name: info description: Show metadata for an object (content type, size, modified date) @@ -1410,6 +1474,7 @@ commands: - "tigris objects info my-bucket report.pdf" - "tigris objects info t3://my-bucket/report.pdf" - "tigris objects info my-bucket report.pdf --format json" + - "tigris objects info my-bucket report.pdf --version-id abc123" messages: onStart: '' onSuccess: '' @@ -1429,6 +1494,8 @@ commands: - name: snapshot-version description: Read from a specific bucket snapshot alias: snapshot + - name: version-id + description: Object version id (requires bucket versioning). Omit to read the latest version ######################### # Manage access keys diff --git a/src/types.ts b/src/types.ts index 68fd9e6..812dd1c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ export interface Argument { name: string; - description: string; + description?: string; alias?: string; options?: | string[] @@ -11,6 +11,10 @@ export interface Argument { type?: 'positional' | 'flag' | string; multiple?: boolean; examples?: string[]; + /** Hard-removed: providing the flag exits with a redirect message. */ + removed?: boolean; + /** Replacement to suggest when a removed argument or command is used. */ + replaced_by?: string; } export interface NextAction { @@ -32,20 +36,21 @@ export interface Messages { // Recursive command structure - supports nth level nesting export interface CommandSpec { name: string; - description: string; + description?: string; alias?: string | string[]; arguments?: Argument[]; examples?: string[]; commands?: CommandSpec[]; // recursive - can nest infinitely default?: string; deprecated?: boolean; + /** Hard-removed: invoking the command exits with a redirect message. */ + removed?: boolean; + /** Replacement to suggest when a removed argument or command is used. */ + replaced_by?: string; message?: string; messages?: Messages; } -// Backwards compatibility alias -export type OperationSpec = CommandSpec; - export interface Specs { name: string; description: string; @@ -61,8 +66,3 @@ export interface ParsedPath { bucket: string; path: string; } - -export interface ParsedPaths { - source: ParsedPath; - destination: ParsedPath; -} diff --git a/src/utils/bucket-info.ts b/src/utils/bucket-info.ts index 4c28dc2..5d9fcb8 100644 --- a/src/utils/bucket-info.ts +++ b/src/utils/bucket-info.ts @@ -1,7 +1,65 @@ -import type { BucketInfoResponse } from '@tigrisdata/storage'; +import type { + BucketInfoResponse, + BucketLifecycleRule, +} from '@tigrisdata/storage'; import { formatSize } from './format.js'; +/** + * Human-readable description of a rule's transition, or undefined if + * the rule has no transition target. Used by both the bucket-info + * "Lifecycle Rules" row and the lifecycle-list table cell. + */ +export function describeTransition( + rule: BucketLifecycleRule +): string | undefined { + if (!rule.storageClass) return undefined; + if (rule.days !== undefined) + return `${rule.storageClass} after ${rule.days}d`; + if (rule.date !== undefined) return `${rule.storageClass} on ${rule.date}`; + return rule.storageClass; +} + +/** + * Human-readable description of a rule's expiration, or undefined if + * the rule has no expiration. Used by both the bucket-info "Lifecycle + * Rules" row and the lifecycle-list table cell. + */ +export function describeExpiration( + rule: BucketLifecycleRule +): string | undefined { + if (!rule.expiration) return undefined; + if (rule.expiration.days !== undefined) return `${rule.expiration.days}d`; + if (rule.expiration.date !== undefined) return rule.expiration.date; + return undefined; +} + +function formatLifecycleRule(rule: BucketLifecycleRule): string { + const parts: string[] = []; + + const transition = describeTransition(rule); + if (transition) parts.push(transition); + + const expiration = describeExpiration(rule); + if (expiration) { + // bucket-info shows expiration with the "expire" prefix; the table + // cell version drops it because the column header already says + // "Expiration". + parts.push( + rule.expiration?.days !== undefined + ? `expire after ${expiration}` + : `expire on ${expiration}` + ); + } + + const annotations: string[] = []; + if (rule.filter?.prefix) annotations.push(`prefix=${rule.filter.prefix}`); + if (rule.enabled === false) annotations.push('disabled'); + + const head = parts.join(', '); + return annotations.length > 0 ? `${head} (${annotations.join(', ')})` : head; +} + export function buildBucketInfo(data: BucketInfoResponse) { const info: { label: string; value: string }[] = [ { @@ -53,25 +111,11 @@ export function buildBucketInfo(data: BucketInfoResponse) { }); } - if (data.settings.ttlConfig) { - info.push({ - label: 'TTL', - value: data.settings.ttlConfig.enabled - ? data.settings.ttlConfig.days - ? `${data.settings.ttlConfig.days} days` - : (data.settings.ttlConfig.date ?? 'Enabled') - : 'Disabled', - }); - } - if (data.settings.lifecycleRules?.length) { info.push({ label: 'Lifecycle Rules', value: data.settings.lifecycleRules - .map( - (r) => - `${r.storageClass}${r.days ? ` after ${r.days}d` : ''}${r.enabled ? '' : ' (disabled)'}` - ) + .map((r) => formatLifecycleRule(r)) .join(', '), }); } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index e785d4e..8178c36 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,4 +1,4 @@ -import type { CommandSpec, Messages, OperationSpec } from '../types.js'; +import type { CommandSpec, Messages } from '../types.js'; import { getCommandSpec } from './specs.js'; export type MessageVariables = Record< @@ -33,7 +33,7 @@ function isJsonMode(): boolean { function getMessages(context: MessageContext): Messages | undefined { const spec = getCommandSpec(context.command, context.operation); if (!spec) return undefined; - return (spec as CommandSpec | OperationSpec).messages; + return (spec as CommandSpec).messages; } /** diff --git a/src/utils/mime.ts b/src/utils/mime.ts new file mode 100644 index 0000000..1ee3402 --- /dev/null +++ b/src/utils/mime.ts @@ -0,0 +1,94 @@ +import { extname } from 'path'; + +/** + * Inline MIME table covering the file types commonly served from + * Tigris buckets. Mirrors the AWS CLI behaviour of `mimetypes.guess_type` + * by extension — extension-only, no content sniffing. Returns + * `undefined` for unknown extensions so callers omit the + * `Content-Type` header and let the server default apply (matches + * `aws s3 cp`'s behaviour, which never emits a fallback + * `application/octet-stream`). + */ +const MIME_TABLE: Record = { + // Markup / scripts + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'text/javascript', + mjs: 'text/javascript', + cjs: 'text/javascript', + json: 'application/json', + map: 'application/json', + xml: 'application/xml', + svg: 'image/svg+xml', + webmanifest: 'application/manifest+json', + wasm: 'application/wasm', + + // Plain text + txt: 'text/plain', + log: 'text/plain', + md: 'text/markdown', + csv: 'text/csv', + yaml: 'application/yaml', + yml: 'application/yaml', + + // Documents + pdf: 'application/pdf', + rtf: 'application/rtf', + + // Images + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + avif: 'image/avif', + ico: 'image/x-icon', + bmp: 'image/bmp', + tif: 'image/tiff', + tiff: 'image/tiff', + + // Fonts + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + otf: 'font/otf', + eot: 'application/vnd.ms-fontobject', + + // Video + mp4: 'video/mp4', + m4v: 'video/x-m4v', + webm: 'video/webm', + mov: 'video/quicktime', + avi: 'video/x-msvideo', + mkv: 'video/x-matroska', + + // Audio + mp3: 'audio/mpeg', + m4a: 'audio/mp4', + wav: 'audio/wav', + ogg: 'audio/ogg', + flac: 'audio/flac', + aac: 'audio/aac', + opus: 'audio/opus', + + // Archives + zip: 'application/zip', + tar: 'application/x-tar', + gz: 'application/gzip', + tgz: 'application/gzip', + bz2: 'application/x-bzip2', + '7z': 'application/x-7z-compressed', + rar: 'application/vnd.rar', +}; + +/** + * Look up a Content-Type from a file path's extension. Returns + * `undefined` when the extension is unknown — callers should omit the + * Content-Type rather than fall back to `application/octet-stream`. + */ +export function getContentType(filePath: string): string | undefined { + const ext = extname(filePath).slice(1).toLowerCase(); + if (!ext) return undefined; + return MIME_TABLE[ext]; +} diff --git a/src/utils/path.ts b/src/utils/path.ts index 41739ba..2182e3e 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,7 +1,7 @@ import type { TigrisStorageConfig } from '@auth/provider.js'; import { list } from '@tigrisdata/storage'; -import type { ParsedPath, ParsedPaths } from '../types.js'; +import type { ParsedPath } from '../types.js'; const REMOTE_PREFIXES = ['t3://', 'tigris://']; @@ -59,19 +59,6 @@ export async function isPathFolder( return !!(data?.items && data.items.length > 0); } -/** - * Parses source and destination paths - * @param src - Source path string - * @param dest - Destination path string - * @returns Object with parsed source and destination - */ -export function parsePaths(src: string, dest: string): ParsedPaths { - return { - source: parsePath(src), - destination: parsePath(dest), - }; -} - /** * Parses a path that may or may not have a t3:// or tigris:// prefix. * Supports both remote prefixed paths and bare bucket/path paths. diff --git a/src/utils/specs.ts b/src/utils/specs.ts index 3bd33fa..85df8c3 100644 --- a/src/utils/specs.ts +++ b/src/utils/specs.ts @@ -3,7 +3,7 @@ import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import * as YAML from 'yaml'; -import type { Argument, CommandSpec, OperationSpec, Specs } from '../types.js'; +import type { Argument, CommandSpec, Specs } from '../types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -30,7 +30,7 @@ export function loadSpecs(): Specs { export function getCommandSpec( commandPath: string, operationName?: string -): OperationSpec | CommandSpec | null { +): CommandSpec | null { const specs = loadSpecs(); // Split command path for nested commands (e.g., "iam policies" -> ["iam", "policies"]) diff --git a/test/cli.test.ts b/test/cli.test.ts index a71cda5..f7cf4d2 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -192,12 +192,11 @@ describe('CLI Help Commands', () => { expect(result.stdout).toContain('Commands:'); }); - it('should show forks help', () => { - const result = runCli('forks help'); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Commands:'); - expect(result.stdout).toContain('list'); - expect(result.stdout).toContain('create'); + it('should print a redirect message for the removed forks command', () => { + const result = runCli('forks'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('removed'); + expect(result.stderr).toContain('--fork-of'); }); it('should show snapshots help', () => { @@ -273,6 +272,9 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { // Generate unique prefix for all test resources const testPrefix = getTestPrefix(); const testBucket = testPrefix; + // Second bucket used by cross-bucket cp/mv tests; created and torn + // down alongside the primary bucket. + const otherBucket = `${testPrefix}-other`; const testContent = 'Hello from CLI test'; /** Prefix a bucket/path with t3:// for commands that require remote paths (cp, mv, rm) */ @@ -316,6 +318,13 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { console.error('Failed to create test bucket:', result.stderr); throw new Error('Failed to create test bucket'); } + + console.log(`Creating second test bucket: ${otherBucket}`); + const otherResult = runCli(`mk ${otherBucket}`); + if (otherResult.exitCode !== 0) { + console.error('Failed to create second test bucket:', otherResult.stderr); + throw new Error('Failed to create second test bucket'); + } }); afterAll(async () => { @@ -323,6 +332,10 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { // Force remove all objects and the bucket runCli(`rm ${t3(testBucket)}/* -f`); runCli(`rm ${t3(testBucket)} -f`); + + console.log(`Cleaning up second test bucket: ${otherBucket}`); + runCli(`rm ${t3(otherBucket)}/* -f`); + runCli(`rm ${t3(otherBucket)} -f`); }); describe('ls command', () => { @@ -998,6 +1011,48 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); }); + describe('cross-bucket cp and mv', () => { + const cpFile = 'xb-cp-source.txt'; + const mvFile = 'xb-mv-source.txt'; + + beforeAll(() => { + runCli(`touch ${testBucket}/${cpFile}`); + runCli(`touch ${testBucket}/${mvFile}`); + }); + + it('should cp across buckets via server-side CopyObject', () => { + const result = runCli( + `cp ${t3(testBucket)}/${cpFile} ${t3(otherBucket)}/${cpFile}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Copied'); + + // Source still present in original bucket + const lsSrc = runCli(`ls ${testBucket}`); + expect(lsSrc.stdout).toContain(cpFile); + + // Destination present in other bucket + const lsDest = runCli(`ls ${otherBucket}`); + expect(lsDest.stdout).toContain(cpFile); + }); + + it('should mv across buckets via server-side copy + remove', () => { + const result = runCli( + `mv ${t3(testBucket)}/${mvFile} ${t3(otherBucket)}/${mvFile} -f` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Moved'); + + // Source gone from original bucket + const lsSrc = runCli(`ls ${testBucket}`); + expect(lsSrc.stdout).not.toContain(mvFile); + + // Destination present in other bucket + const lsDest = runCli(`ls ${otherBucket}`); + expect(lsDest.stdout).toContain(mvFile); + }); + }); + describe('mv command - additional branches', () => { it('should move objects matching wildcard with -f', () => { runCli(`touch ${testBucket}/mv-wc-a.txt`); @@ -1438,65 +1493,12 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); }); - describe('buckets set-ttl', () => { - it('should set TTL with --days 30', () => { + describe('buckets set-ttl (removed in v3)', () => { + it('should print a redirect message pointing at lifecycle', () => { const result = runCli(`buckets set-ttl ${setBucket} --days 30`); - expect(result.exitCode).toBe(0); - }); - - it('should set TTL with --date 2027-01-01', () => { - const result = runCli(`buckets set-ttl ${setBucket} --date 2027-01-01`); - expect(result.exitCode).toBe(0); - }); - - it('should enable with --enable', () => { - const result = runCli(`buckets set-ttl ${setBucket} --enable`); - expect(result.exitCode).toBe(0); - }); - - it('should disable with --disable', () => { - const result = runCli(`buckets set-ttl ${setBucket} --disable`); - expect(result.exitCode).toBe(0); - }); - - it('should error when using both --enable and --disable', () => { - const result = runCli( - `buckets set-ttl ${setBucket} --enable --disable` - ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Cannot use both --enable and --disable' - ); - }); - - it('should error when using --disable with --days', () => { - const result = runCli( - `buckets set-ttl ${setBucket} --disable --days 30` - ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Cannot use --disable with --days or --date' - ); - }); - - it('should error on invalid --days', () => { - const result = runCli(`buckets set-ttl ${setBucket} --days -5`); expect(result.exitCode).toBe(1); - expect(result.stderr).toContain('--days must be a positive number'); - }); - - it('should error on invalid --date', () => { - const result = runCli(`buckets set-ttl ${setBucket} --date not-a-date`); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain('--date must be a valid ISO-8601 date'); - }); - - it('should error when no action provided', () => { - const result = runCli(`buckets set-ttl ${setBucket}`); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Provide --days, --date, --enable, or --disable' - ); + expect(result.stderr).toContain('removed'); + expect(result.stderr).toContain('lifecycle'); }); }); @@ -1534,83 +1536,14 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); }); - describe('buckets set-transition', () => { - it('should set with --days 30 --storage-class GLACIER', () => { + describe('buckets set-transition (removed in v3)', () => { + it('should print a redirect message pointing at lifecycle', () => { const result = runCli( `buckets set-transition ${setBucket} --days 30 --storage-class GLACIER` ); - expect(result.exitCode).toBe(0); - }); - - it('should set with --date 2027-01-01 --storage-class GLACIER_IR', () => { - const result = runCli( - `buckets set-transition ${setBucket} --date 2027-01-01 --storage-class GLACIER_IR` - ); - expect(result.exitCode).toBe(0); - }); - - it('should enable with --enable', () => { - const result = runCli(`buckets set-transition ${setBucket} --enable`); - expect(result.exitCode).toBe(0); - }); - - it('should disable with --disable', () => { - const result = runCli(`buckets set-transition ${setBucket} --disable`); - expect(result.exitCode).toBe(0); - }); - - it('should error on invalid storage class STANDARD', () => { - const result = runCli( - `buckets set-transition ${setBucket} --days 30 --storage-class STANDARD` - ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'STANDARD is not a valid transition target' - ); - }); - - it('should error on --days without --storage-class', () => { - const result = runCli(`buckets set-transition ${setBucket} --days 30`); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - '--storage-class is required when setting --days or --date' - ); - }); - - it('should error when using both --enable and --disable', () => { - const result = runCli( - `buckets set-transition ${setBucket} --enable --disable` - ); expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Cannot use both --enable and --disable' - ); - }); - - it('should error on --disable with --days', () => { - const result = runCli( - `buckets set-transition ${setBucket} --disable --days 30` - ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Cannot use --disable with --days, --date, or --storage-class' - ); - }); - - it('should error when no action provided', () => { - const result = runCli(`buckets set-transition ${setBucket}`); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain( - 'Provide --days, --date, --enable, or --disable' - ); - }); - - it('should error on invalid --days', () => { - const result = runCli( - `buckets set-transition ${setBucket} --days -1 --storage-class GLACIER` - ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain('--days must be a positive number'); + expect(result.stderr).toContain('removed'); + expect(result.stderr).toContain('lifecycle'); }); }); @@ -1812,6 +1745,53 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); }); + describe('objects set-access command', () => { + const testFile = 'setaccess-test.txt'; + + beforeAll(() => { + runCli(`touch ${testBucket}/${testFile}`); + }); + + afterAll(() => { + runCli(`rm ${t3(testBucket)}/${testFile} -f`); + }); + + it('should set access to public via positional', () => { + const result = runCli( + `objects set-access ${testBucket} ${testFile} public` + ); + expect(result.exitCode).toBe(0); + }); + + it('should set access to private via positional', () => { + const result = runCli( + `objects set-access ${testBucket} ${testFile} private` + ); + expect(result.exitCode).toBe(0); + }); + + it('should accept t3:// path with access as second positional', () => { + const result = runCli( + `objects set-access ${t3(testBucket)}/${testFile} public` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error when the access positional is missing', () => { + const result = runCli(`objects set-access ${testBucket} ${testFile}`); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Access level is required'); + }); + + it('should error on invalid access value', () => { + const result = runCli( + `objects set-access ${testBucket} ${testFile} maybe` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Access level must be either'); + }); + }); + describe('objects commands with t3:// paths', () => { const tmpBase = join(tmpdir(), `cli-test-t3path-${testPrefix}`); @@ -1977,27 +1957,18 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { expect(result.stdout).toContain('Size'); }); - it('should create a fork via forks create', () => { - const result = runCli(`forks create ${snapBucket} ${forkBucket}`); + it('should create a fork via buckets create --fork-of', () => { + const result = runCli( + `buckets create ${forkBucket} --fork-of ${snapBucket}` + ); expect(result.exitCode).toBe(0); }); - it('should list forks', () => { - // Retry — fork visibility is eventually consistent - let result = { stdout: '', stderr: '', exitCode: 1 }; - for (let i = 0; i < 3; i++) { - result = runCli(`forks list ${snapBucket}`); - if (result.exitCode === 0 && result.stdout.includes(forkBucket)) break; - if (i < 2) execSync('sleep 5'); - } - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain(forkBucket); - }, 120_000); - - it('should list forks with --format json', () => { - const result = runCli(`forks list ${snapBucket} --format json`); + it('should list forks via buckets list --forks-of (json)', () => { + const result = runCli( + `buckets list --forks-of ${snapBucket} --format json` + ); expect(result.exitCode).toBe(0); - // May return JSON array or empty (printEmpty is TTY-gated) if (result.stdout.trim()) { expect(() => JSON.parse(result.stdout.trim())).not.toThrow(); } diff --git a/test/lib/buckets/lifecycle/shared.test.ts b/test/lib/buckets/lifecycle/shared.test.ts new file mode 100644 index 0000000..9423dc2 --- /dev/null +++ b/test/lib/buckets/lifecycle/shared.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; + +import { + expirationFromInput, + transitionDeltaFromInput, + validateRuleFieldCombinations, +} from '../../../../src/lib/buckets/lifecycle/shared.js'; + +describe('transitionDeltaFromInput', () => { + it('returns only the storage class when only --storage-class is set', () => { + const delta = transitionDeltaFromInput({ storageClass: 'GLACIER' }); + expect(delta).toEqual({ storageClass: 'GLACIER' }); + }); + + it('emits days and explicitly clears date when only --days is set', () => { + // Regression: spreading ...target before this delta needs to override + // an existing target.date. Without `date: undefined`, the spread leaves + // both populated and the API rejects the rule. + const delta = transitionDeltaFromInput({ days: '30' }); + expect(delta).toEqual({ days: 30, date: undefined }); + }); + + it('emits date and explicitly clears days when only --date is set', () => { + const delta = transitionDeltaFromInput({ date: '2026-06-01' }); + expect(delta).toEqual({ date: '2026-06-01', days: undefined }); + }); + + it('returns an empty delta when no timing or class is set', () => { + const delta = transitionDeltaFromInput({}); + expect(delta).toEqual({}); + }); +}); + +describe('validateRuleFieldCombinations', () => { + it('rejects --days without --storage-class by default (create semantics)', () => { + expect(validateRuleFieldCombinations({ days: '30' })).toContain( + '--storage-class is required' + ); + }); + + it('allows --days without --storage-class when called with requireStorageClassForTiming: false (edit semantics)', () => { + // Regression: validator used to block edit cases where the existing + // rule already had a storage class. + expect( + validateRuleFieldCombinations( + { days: '30' }, + { requireStorageClassForTiming: false } + ) + ).toBeUndefined(); + }); + + it('still rejects mutually exclusive --days and --date in edit mode', () => { + expect( + validateRuleFieldCombinations( + { days: '30', date: '2026-06-01' }, + { requireStorageClassForTiming: false } + ) + ).toContain('Cannot specify both --days and --date'); + }); + + it('rejects an empty --prefix', () => { + expect(validateRuleFieldCombinations({ prefix: '' })).toContain( + '--prefix cannot be empty' + ); + }); +}); + +describe('expirationFromInput', () => { + it('emits days and explicitly clears date when --expire-days is set', () => { + const expiration = expirationFromInput({ expireDays: '7' }); + expect(expiration).toEqual({ days: 7, date: undefined }); + }); + + it('emits date and explicitly clears days when --expire-date is set', () => { + const expiration = expirationFromInput({ expireDate: '2026-12-31' }); + expect(expiration).toEqual({ date: '2026-12-31', days: undefined }); + }); + + it('returns undefined when neither expire flag is set', () => { + expect(expirationFromInput({})).toBeUndefined(); + }); +}); diff --git a/test/specs-completeness.test.ts b/test/specs-completeness.test.ts index cbb4471..059f427 100644 --- a/test/specs-completeness.test.ts +++ b/test/specs-completeness.test.ts @@ -23,6 +23,9 @@ function collectLeaves( const leaves: LeafCommand[] = []; for (const cmd of commands) { + // Removed commands are tombstones — no handler, no messages block. + if (cmd.removed) continue; + const currentPath = [...parentPath, cmd.name]; if (!cmd.commands || cmd.commands.length === 0) { diff --git a/test/utils/bucket-info.test.ts b/test/utils/bucket-info.test.ts index c2f9ded..86b8b81 100644 --- a/test/utils/bucket-info.test.ts +++ b/test/utils/bucket-info.test.ts @@ -148,61 +148,6 @@ describe('buildBucketInfo', () => { }); }); - describe('TTL config', () => { - it('does not add TTL when ttlConfig is undefined', () => { - const info = buildBucketInfo(makeResponse()); - expect(findValue(info, 'TTL')).toBeUndefined(); - }); - - it('shows Disabled when ttlConfig.enabled is false', () => { - const info = buildBucketInfo( - makeResponse({ - settings: { - ...makeResponse().settings, - ttlConfig: { enabled: false }, - }, - }) - ); - expect(findValue(info, 'TTL')).toBe('Disabled'); - }); - - it('shows days when enabled with days', () => { - const info = buildBucketInfo( - makeResponse({ - settings: { - ...makeResponse().settings, - ttlConfig: { enabled: true, days: 30 }, - }, - }) - ); - expect(findValue(info, 'TTL')).toBe('30 days'); - }); - - it('shows date when enabled without days', () => { - const info = buildBucketInfo( - makeResponse({ - settings: { - ...makeResponse().settings, - ttlConfig: { enabled: true, date: '2025-12-31' }, - }, - }) - ); - expect(findValue(info, 'TTL')).toBe('2025-12-31'); - }); - - it('shows Enabled when enabled without days or date', () => { - const info = buildBucketInfo( - makeResponse({ - settings: { - ...makeResponse().settings, - ttlConfig: { enabled: true }, - }, - }) - ); - expect(findValue(info, 'TTL')).toBe('Enabled'); - }); - }); - describe('lifecycle rules', () => { it('does not add lifecycle rules when undefined', () => { const info = buildBucketInfo(makeResponse()); @@ -264,6 +209,29 @@ describe('buildBucketInfo', () => { 'STANDARD_IA after 30d, GLACIER after 90d' ); }); + + it('renders TTL-shaped rules (expiration only) alongside transitions', () => { + const info = buildBucketInfo( + makeResponse({ + settings: { + ...makeResponse().settings, + lifecycleRules: [ + { id: 'ttl-1', expiration: { days: 7 }, enabled: true }, + { + id: 'lc-1', + storageClass: 'GLACIER', + days: 90, + enabled: true, + }, + ], + }, + }) + ); + expect(findValue(info, 'TTL')).toBeUndefined(); + expect(findValue(info, 'Lifecycle Rules')).toBe( + 'expire after 7d, GLACIER after 90d' + ); + }); }); describe('CORS rules', () => { diff --git a/test/utils/mime.test.ts b/test/utils/mime.test.ts new file mode 100644 index 0000000..7b06494 --- /dev/null +++ b/test/utils/mime.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; + +import { getContentType } from '../../src/utils/mime.js'; + +describe('getContentType', () => { + it('returns text/html for .html', () => { + expect(getContentType('foo.html')).toBe('text/html'); + expect(getContentType('a/b/index.html')).toBe('text/html'); + }); + + it('handles uppercase extensions (lowercases internally)', () => { + expect(getContentType('IMAGE.PNG')).toBe('image/png'); + expect(getContentType('Foo.JPG')).toBe('image/jpeg'); + }); + + it('matches the final extension only (.tar.gz → gzip)', () => { + expect(getContentType('archive.tar.gz')).toBe('application/gzip'); + }); + + it('returns text/javascript for .js / .mjs / .cjs', () => { + expect(getContentType('app.js')).toBe('text/javascript'); + expect(getContentType('app.mjs')).toBe('text/javascript'); + expect(getContentType('app.cjs')).toBe('text/javascript'); + }); + + it('returns image/svg+xml for .svg', () => { + expect(getContentType('logo.svg')).toBe('image/svg+xml'); + }); + + it('returns undefined when the extension is unknown', () => { + // AWS-CLI behavior parity: callers omit the header and let the + // server default apply rather than emitting application/octet-stream. + expect(getContentType('mystery.xyz')).toBeUndefined(); + }); + + it('returns undefined when there is no extension', () => { + expect(getContentType('Makefile')).toBeUndefined(); + expect(getContentType('binary')).toBeUndefined(); + }); + + it('returns undefined for dotfiles (no extension after the dot)', () => { + // extname('.gitignore') === '' — these are treated as no-extension. + expect(getContentType('.gitignore')).toBeUndefined(); + expect(getContentType('.env')).toBeUndefined(); + }); +}); diff --git a/test/utils/path.test.ts b/test/utils/path.test.ts index 941ef92..26e1743 100644 --- a/test/utils/path.test.ts +++ b/test/utils/path.test.ts @@ -5,7 +5,6 @@ import { isRemotePath, parseAnyPath, parsePath, - parsePaths, parseRemotePath, resolveObjectArgs, wildcardPrefix, @@ -49,27 +48,6 @@ describe('parsePath', () => { }); }); -describe('parsePaths', () => { - it('should parse source and destination paths', () => { - const result = parsePaths( - 'src-bucket/file.txt', - 'dest-bucket/new-file.txt' - ); - expect(result.source.bucket).toBe('src-bucket'); - expect(result.source.path).toBe('file.txt'); - expect(result.destination.bucket).toBe('dest-bucket'); - expect(result.destination.path).toBe('new-file.txt'); - }); - - it('should handle cross-bucket copy with same filename', () => { - const result = parsePaths('bucket-a/folder/file.txt', 'bucket-b'); - expect(result.source.bucket).toBe('bucket-a'); - expect(result.source.path).toBe('folder/file.txt'); - expect(result.destination.bucket).toBe('bucket-b'); - expect(result.destination.path).toBe(''); - }); -}); - describe('isRemotePath', () => { it('should return true for t3:// prefixed paths', () => { expect(isRemotePath('t3://my-bucket')).toBe(true);