From 3401e8062ce9187bdaa7f75b471ca3e463855509 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Thu, 12 Mar 2026 10:47:12 +0100 Subject: [PATCH 1/7] fix: release channel for beta releases (#35) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 50f07c7..173ef89 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "release", { "name": "main", - "prerelease": true + "prerelease": "beta", + "channel": "beta" } ], "plugins": [ From d97089f7ca93b9dfbf5a5a3c41b8d278633c0605 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Thu, 12 Mar 2026 13:48:43 +0100 Subject: [PATCH 2/7] feat: implement buckets set locations - feat: implement buckets set locations - chore: fix tsconfig --- src/lib/buckets/create.ts | 64 ++--------------- src/lib/buckets/set-locations.ts | 59 ++++++++++++++++ src/specs.yaml | 23 +++++++ src/utils/locations.ts | 113 +++++++++++++++++++++++++++---- tsconfig.json | 2 +- 5 files changed, 189 insertions(+), 72 deletions(-) create mode 100644 src/lib/buckets/set-locations.ts diff --git a/src/lib/buckets/create.ts b/src/lib/buckets/create.ts index 52f030e..91ca746 100644 --- a/src/lib/buckets/create.ts +++ b/src/lib/buckets/create.ts @@ -3,11 +3,7 @@ import enquirer from 'enquirer'; import { getArgumentSpec, buildPromptChoices } from '../../utils/specs.js'; import { StorageClass, createBucket } from '@tigrisdata/storage'; import { getStorageConfig } from '../../auth/s3-client'; -import { - parseLocations, - multiRegionChoices, - singleRegionChoices, -} from '../../utils/locations.js'; +import { parseLocations, promptLocations } from '../../utils/locations.js'; import type { BucketLocations } from '@tigrisdata/storage'; import { printStart, @@ -116,59 +112,11 @@ export default async function create(options: Record) { defaultTier = responses.defaultTier; enableSnapshots = responses.enableSnapshots; - // Location selection: type first, then region(s) based on type - const { locationType } = await prompt<{ locationType: string }>({ - type: 'select', - name: 'locationType', - message: 'Location type:', - choices: [ - { name: 'global', message: 'Global' }, - { name: 'multi', message: 'Multi-region (USA or Europe)' }, - { name: 'dual', message: 'Dual region' }, - { name: 'single', message: 'Single region' }, - ], - }); - - if (locationType === 'global') { - parsedLocations = { type: 'global' }; - } else if (locationType === 'multi') { - const { region } = await prompt<{ region: string }>({ - type: 'select', - name: 'region', - message: 'Multi-region:', - choices: multiRegionChoices.map((c) => ({ - name: c.value, - message: c.name, - })), - }); - parsedLocations = parseLocations(region); - } else if (locationType === 'single') { - const { region } = await prompt<{ region: string }>({ - type: 'select', - name: 'region', - message: 'Region:', - choices: singleRegionChoices.map((c) => ({ - name: c.value, - message: c.name, - })), - }); - parsedLocations = parseLocations(region); - } else { - const { regions } = await prompt<{ regions: string[] }>({ - type: 'multiselect', - name: 'regions', - message: - 'Press space key to select regions (multiple supported) and enter to confirm:', - choices: singleRegionChoices.map((c) => ({ - name: c.value, - message: c.name, - })), - } as Parameters[0]); - if (regions.length < 2) { - printFailure(context, 'Dual region requires at least two regions'); - process.exit(1); - } - parsedLocations = parseLocations(regions); + try { + parsedLocations = await promptLocations(); + } catch (err) { + printFailure(context, (err as Error).message); + process.exit(1); } } diff --git a/src/lib/buckets/set-locations.ts b/src/lib/buckets/set-locations.ts new file mode 100644 index 0000000..4b03ef3 --- /dev/null +++ b/src/lib/buckets/set-locations.ts @@ -0,0 +1,59 @@ +import { getOption } from '../../utils/options.js'; +import { getStorageConfig } from '../../auth/s3-client.js'; +import { getSelectedOrganization } from '../../auth/storage.js'; +import { updateBucket } from '@tigrisdata/storage'; +import type { BucketLocations } from '@tigrisdata/storage'; +import { parseLocations, promptLocations } from '../../utils/locations.js'; +import { + printStart, + printSuccess, + printFailure, + msg, +} from '../../utils/messages.js'; + +const context = msg('buckets', 'set-locations'); + +export default async function setLocations(options: Record) { + printStart(context); + + const name = getOption(options, ['name']); + const locations = getOption(options, ['locations']); + + if (!name) { + printFailure(context, 'Bucket name is required'); + process.exit(1); + } + + let parsedLocations: BucketLocations; + if (locations !== undefined) { + parsedLocations = parseLocations(locations); + } else { + try { + parsedLocations = await promptLocations(); + } catch (err) { + printFailure(context, (err as Error).message); + process.exit(1); + } + } + + const config = await getStorageConfig(); + const selectedOrg = getSelectedOrganization(); + const finalConfig = { + ...config, + ...(selectedOrg && !config.organizationId + ? { organizationId: selectedOrg } + : {}), + }; + + const { error } = await updateBucket(name, { + locations: parsedLocations, + config: finalConfig, + }); + + if (error) { + printFailure(context, error.message); + process.exit(1); + } + + printSuccess(context, { name }); +} diff --git a/src/specs.yaml b/src/specs.yaml index 1299df9..2ef0d08 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -747,6 +747,29 @@ commands: - name: disable description: Disable TTL on the bucket type: flag + # set-locations + - name: set-locations + description: Set the data locations for a bucket + examples: + - "tigris buckets set-locations my-bucket --locations iad" + - "tigris buckets set-locations my-bucket --locations iad,fra" + - "tigris buckets set-locations my-bucket --locations global" + messages: + onStart: 'Updating bucket locations...' + onSuccess: 'Locations updated for bucket {{name}}' + onFailure: 'Failed to update bucket locations' + arguments: + - name: name + description: Name of the bucket + type: positional + required: true + examples: + - my-bucket + - name: locations + description: Bucket location + alias: l + options: *location_options + multiple: true # set-migration - name: set-migration description: Configure data migration from an external S3-compatible source bucket. Tigris will pull objects on demand from the source diff --git a/src/utils/locations.ts b/src/utils/locations.ts index ad95e06..7e27fab 100644 --- a/src/utils/locations.ts +++ b/src/utils/locations.ts @@ -1,4 +1,7 @@ import type { BucketLocations } from '@tigrisdata/storage'; +import enquirer from 'enquirer'; + +const { prompt } = enquirer; type Multi = Extract; type Single = Extract; @@ -6,25 +9,109 @@ type Dual = Extract; const multiRegions: string[] = ['usa', 'eur']; -export const multiRegionChoices = [ +const multiRegionChoices = [ { name: 'USA', value: 'usa' }, { name: 'Europe', value: 'eur' }, ]; -export const singleRegionChoices = [ - { name: 'Amsterdam, Netherlands', value: 'ams' }, - { name: 'Frankfurt, Germany', value: 'fra' }, - { name: 'Sao Paulo, Brazil', value: 'gru' }, - { name: 'Ashburn, Virginia (US)', value: 'iad' }, - { name: 'Johannesburg, South Africa', value: 'jnb' }, - { name: 'London, United Kingdom', value: 'lhr' }, - { name: 'Tokyo, Japan', value: 'nrt' }, - { name: 'Chicago, Illinois (US)', value: 'ord' }, - { name: 'Singapore, Singapore', value: 'sin' }, - { name: 'San Jose, California (US)', value: 'sjc' }, - { name: 'Sydney, Australia', value: 'syd' }, +const singleRegionChoices = [ + { name: 'Amsterdam, Netherlands (AMS)', value: 'ams' }, + { name: 'Frankfurt, Germany (FRA)', value: 'fra' }, + { name: 'Sao Paulo, Brazil (GRU)', value: 'gru' }, + { name: 'Ashburn, Virginia (IAD)', value: 'iad' }, + { name: 'Johannesburg, South Africa (JNB)', value: 'jnb' }, + { name: 'London, United Kingdom (LHR)', value: 'lhr' }, + { name: 'Tokyo, Japan (NRT)', value: 'nrt' }, + { name: 'Chicago, Illinois (ORD)', value: 'ord' }, + { name: 'Singapore, Singapore (SIN)', value: 'sin' }, + { name: 'San Jose, California (SJC)', value: 'sjc' }, + { name: 'Sydney, Australia (SYD)', value: 'syd' }, ]; +async function promptRegion( + locationType: string +): Promise { + try { + if (locationType === 'multi') { + const { region } = await prompt<{ region: string }>({ + type: 'select', + name: 'region', + message: 'Multi-region:', + choices: multiRegionChoices.map((c) => ({ + name: c.value, + message: c.name, + })), + }); + return parseLocations(region); + } + + if (locationType === 'single') { + const { region } = await prompt<{ region: string }>({ + type: 'select', + name: 'region', + message: 'Region:', + choices: singleRegionChoices.map((c) => ({ + name: c.value, + message: c.name, + })), + }); + return parseLocations(region); + } + + // dual + const { regions } = await prompt<{ regions: string[] }>({ + type: 'multiselect', + name: 'regions', + message: + 'Press space key to select regions (multiple supported) and enter to confirm:', + choices: singleRegionChoices.map((c) => ({ + name: c.value, + message: c.name, + })), + } as Parameters[0]); + + if (regions.length < 2) { + throw new Error('Dual region requires at least two regions'); + } + + return parseLocations(regions); + } catch (err) { + // User pressed Escape — return null to go back + if (err === '') { + return null; + } + + throw err; + } +} + +export async function promptLocations(): Promise { + let locationType: string; + try { + ({ locationType } = await prompt<{ locationType: string }>({ + type: 'select', + name: 'locationType', + message: 'Location type:', + choices: [ + { name: 'global', message: 'Global' }, + { name: 'multi', message: 'Multi-region (USA or Europe)' }, + { name: 'dual', message: 'Dual region' }, + { name: 'single', message: 'Single region' }, + ], + })); + } catch { + throw new Error('Location selection cancelled'); + } + + if (locationType === 'global') { + return { type: 'global' }; + } + + const result = await promptRegion(locationType); + // Escape was pressed in sub-menu — go back to location type + return result ?? promptLocations(); +} + export function parseLocations(input: string | string[]): BucketLocations { const values = (Array.isArray(input) ? input : [input]) .flatMap((v) => v.split(',')) diff --git a/tsconfig.json b/tsconfig.json index 6a24223..208cdd0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "moduleDetection": "force" }, "include": ["src"], - "exclude": ["src/cli-binary.ts", "src/specs-embedded.ts", "src/command-registry.ts"] + "exclude": ["src/cli-binary.ts", "src/command-registry.ts"] } From db1c51d3ab8986cc570c6bf5a784f118552c3eb0 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Fri, 13 Mar 2026 09:37:44 +0100 Subject: [PATCH 3/7] feat: support snapshot version in object commands (#37) --- src/cli-core.ts | 15 ++++++++++----- src/lib/ls.ts | 6 ++++++ src/lib/objects/get.ts | 7 +++++++ src/lib/objects/list.ts | 6 ++++++ src/lib/stat.ts | 23 +++++++++++++---------- src/specs.yaml | 12 ++++++++++++ 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/cli-core.ts b/src/cli-core.ts index e825109..c42fe0d 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -59,8 +59,9 @@ export function formatArgumentHelp(arg: Argument): string { optionPart = ` ${arg.name}`; } else { optionPart = ` --${arg.name}`; - if (arg.alias && typeof arg.alias === 'string' && arg.alias.length === 1) { - optionPart += `, -${arg.alias}`; + if (arg.alias && typeof arg.alias === 'string') { + optionPart += + arg.alias.length === 1 ? `, -${arg.alias}` : `, --${arg.alias}`; } } @@ -222,11 +223,15 @@ export function addArgumentsToCommand( const argumentName = arg.required ? `<${arg.name}>` : `[${arg.name}]`; cmd.argument(argumentName, arg.description); } else { - const hasValidShortOption = + const isShortAlias = arg.alias && typeof arg.alias === 'string' && arg.alias.length === 1; - let optionString = hasValidShortOption + const isLongAlias = + arg.alias && typeof arg.alias === 'string' && arg.alias.length > 1; + let optionString = isShortAlias ? `-${arg.alias}, --${arg.name}` - : `--${arg.name}`; + : isLongAlias + ? `--${arg.alias}, --${arg.name}` + : `--${arg.name}`; if (arg.type === 'flag') { // Flags don't take values diff --git a/src/lib/ls.ts b/src/lib/ls.ts index ed5b940..a7682ce 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -6,6 +6,11 @@ import { list, listBuckets } from '@tigrisdata/storage'; export default async function ls(options: Record) { const pathString = getOption(options, ['path']); + const snapshotVersion = getOption(options, [ + 'snapshot-version', + 'snapshotVersion', + 'snapshot', + ]); if (!pathString) { // No path provided, list all buckets @@ -45,6 +50,7 @@ export default async function ls(options: Record) { const { data, error } = await list({ prefix, + ...(snapshotVersion ? { snapshotVersion } : {}), config: { ...config, bucket, diff --git a/src/lib/objects/get.ts b/src/lib/objects/get.ts index 1c8469f..7c7ae16 100644 --- a/src/lib/objects/get.ts +++ b/src/lib/objects/get.ts @@ -112,6 +112,11 @@ export default async function getObject(options: Record) { const key = getOption(options, ['key']); const output = getOption(options, ['output', 'o', 'O']); const modeOption = getOption(options, ['mode', 'm', 'M']); + const snapshotVersion = getOption(options, [ + 'snapshot-version', + 'snapshotVersion', + 'snapshot', + ]); if (!bucket) { printFailure(context, 'Bucket name is required'); @@ -130,6 +135,7 @@ export default async function getObject(options: Record) { if (mode === 'stream') { const { data, error } = await get(key, 'stream', { + ...(snapshotVersion ? { snapshotVersion } : {}), config: { ...config, bucket, @@ -152,6 +158,7 @@ export default async function getObject(options: Record) { } } else { const { data, error } = await get(key, 'string', { + ...(snapshotVersion ? { snapshotVersion } : {}), config: { ...config, bucket, diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index af8ccdc..5f83235 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -18,6 +18,11 @@ export default async function listObjects(options: Record) { const bucket = getOption(options, ['bucket']); const prefix = getOption(options, ['prefix', 'p', 'P']); const format = getOption(options, ['format', 'f', 'F'], 'table'); + const snapshotVersion = getOption(options, [ + 'snapshot-version', + 'snapshotVersion', + 'snapshot', + ]); if (!bucket) { printFailure(context, 'Bucket name is required'); @@ -28,6 +33,7 @@ export default async function listObjects(options: Record) { const { data, error } = await list({ prefix: prefix || undefined, + ...(snapshotVersion ? { snapshotVersion } : {}), config: { ...config, bucket, diff --git a/src/lib/stat.ts b/src/lib/stat.ts index 7bfd51c..76abb67 100644 --- a/src/lib/stat.ts +++ b/src/lib/stat.ts @@ -1,4 +1,5 @@ import { parseAnyPath } from '../utils/path.js'; +import { getOption } from '../utils/options.js'; import { formatOutput, formatSize } from '../utils/format.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { getStats, getBucketInfo, head } from '@tigrisdata/storage'; @@ -12,15 +13,16 @@ import { buildBucketInfo } from '../utils/bucket-info.js'; const context = msg('stat'); -export default async function stat(options: { - path?: string; - format?: string; - _positional?: string[]; -}) { +export default async function stat(options: Record) { printStart(context); - const pathString = options.path || options._positional?.[0]; - const format = options.format || 'table'; + const pathString = getOption(options, ['path']); + const format = getOption(options, ['format'], 'table'); + const snapshotVersion = getOption(options, [ + 'snapshot-version', + 'snapshotVersion', + 'snapshot', + ]); const config = await getStorageConfig(); // No path: show overall stats @@ -45,7 +47,7 @@ export default async function stat(options: { }, ]; - const output = formatOutput(stats, format, 'stats', 'stat', [ + const output = formatOutput(stats, format!, 'stats', 'stat', [ { key: 'metric', header: 'Metric' }, { key: 'value', header: 'Value' }, ]); @@ -76,7 +78,7 @@ export default async function stat(options: { value, })); - const output = formatOutput(info, format, 'bucket-info', 'info', [ + const output = formatOutput(info, format!, 'bucket-info', 'info', [ { key: 'metric', header: 'Metric' }, { key: 'value', header: 'Value' }, ]); @@ -88,6 +90,7 @@ export default async function stat(options: { // Object path: show object metadata const { data, error } = await head(path, { + ...(snapshotVersion ? { snapshotVersion } : {}), config: { ...config, bucket, @@ -112,7 +115,7 @@ export default async function stat(options: { { metric: 'Modified', value: data.modified.toISOString() }, ]; - const output = formatOutput(info, format, 'object-info', 'info', [ + const output = formatOutput(info, format!, 'object-info', 'info', [ { key: 'metric', header: 'Metric' }, { key: 'value', header: 'Value' }, ]); diff --git a/src/specs.yaml b/src/specs.yaml index 2ef0d08..4a96fe6 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -249,6 +249,9 @@ commands: - my-bucket/my-path - t3://my-bucket - t3://my-bucket/my-path + - 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 # mk - name: mk @@ -347,6 +350,9 @@ commands: alias: f options: [json, table, xml] default: table + - 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 # presign - name: presign @@ -1086,6 +1092,9 @@ commands: alias: f options: [json, table, xml] default: table + - 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 # get - name: get description: Download an object by key. Prints to stdout by default, or saves to a file with --output @@ -1117,6 +1126,9 @@ commands: description: 'Response mode: "string" loads into memory, "stream" writes in chunks (auto-detected from extension if not specified)' alias: m options: [string, stream] + - 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 # put - name: put description: Upload a local file as an object. Content-type is auto-detected from extension unless overridden From f643218ba9e6b07dda86c8d900bff1729ce4c3e4 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Fri, 13 Mar 2026 11:55:38 +0100 Subject: [PATCH 4/7] fix: merge forking operations in bucket commands (#38) --- src/cli-core.ts | 9 +++++++ src/lib/buckets/create.ts | 13 ++++++++++ src/lib/buckets/list.ts | 50 +++++++++++++++++++++++++++++++++++---- src/lib/mk.ts | 13 ++++++++++ src/specs.yaml | 33 ++++++++++++++++++++++---- src/types.ts | 2 ++ src/utils/messages.ts | 9 +++++++ 7 files changed, 120 insertions(+), 9 deletions(-) diff --git a/src/cli-core.ts b/src/cli-core.ts index c42fe0d..fff3d63 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -4,6 +4,7 @@ import { Command as CommanderCommand } from 'commander'; import type { Argument, CommandSpec, Specs } from './types.js'; +import { printDeprecated } from './utils/messages.js'; export interface ModuleLoader { (commandPath: string[]): Promise<{ @@ -448,6 +449,10 @@ export function registerCommands( return; } + if (defaultCmd.deprecated && defaultCmd.messages?.onDeprecated) { + printDeprecated(defaultCmd.messages.onDeprecated); + } + await loadAndExecuteCommand( loadModule, [...currentPath, defaultCmd.name], @@ -479,6 +484,10 @@ export function registerCommands( return; } + if (spec.deprecated && spec.messages?.onDeprecated) { + printDeprecated(spec.messages.onDeprecated); + } + await loadAndExecuteCommand( loadModule, currentPath, diff --git a/src/lib/buckets/create.ts b/src/lib/buckets/create.ts index 91ca746..c63e8f9 100644 --- a/src/lib/buckets/create.ts +++ b/src/lib/buckets/create.ts @@ -31,6 +31,12 @@ export default async function create(options: Record) { ]); let defaultTier = getOption(options, ['default-tier', 't', 'T']); let locations = getOption(options, ['locations', 'l', 'L']); + const forkOf = getOption(options, ['fork-of', 'forkOf', 'fork']); + const sourceSnapshot = getOption(options, [ + 'source-snapshot', + 'sourceSnapshot', + 'source-snap', + ]); // Handle deprecated --region and --consistency options const deprecatedRegion = getOption(options, ['region', 'r', 'R']); @@ -125,11 +131,18 @@ export default async function create(options: Record) { process.exit(1); } + if (sourceSnapshot && !forkOf) { + printFailure(context, '--source-snapshot requires --fork-of'); + process.exit(1); + } + const { error } = await createBucket(name, { defaultTier: (defaultTier ?? 'STANDARD') as StorageClass, enableSnapshot: enableSnapshots === true, access: (access ?? 'private') as 'public' | 'private', locations: parsedLocations ?? parseLocations(locations ?? 'global'), + ...(forkOf ? { sourceBucketName: forkOf } : {}), + ...(sourceSnapshot ? { sourceBucketSnapshot: sourceSnapshot } : {}), config: await getStorageConfig(), }); diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index ca176a3..02387b8 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -1,7 +1,7 @@ import { getOption } from '../../utils/options.js'; import { formatOutput } from '../../utils/format.js'; import { getStorageConfig } from '../../auth/s3-client.js'; -import { listBuckets } from '@tigrisdata/storage'; +import { listBuckets, getBucketInfo } from '@tigrisdata/storage'; import { printStart, printSuccess, @@ -17,10 +17,10 @@ export default async function list(options: Record) { try { const format = getOption(options, ['format', 'F'], 'table'); + const forksOf = getOption(options, ['forks-of', 'forksOf']); + const config = await getStorageConfig(); - const { data, error } = await listBuckets({ - config: await getStorageConfig(), - }); + const { data, error } = await listBuckets({ config }); if (error) { printFailure(context, error.message); @@ -32,6 +32,48 @@ export default async function list(options: Record) { return; } + if (forksOf) { + // Filter for forks of the named source bucket + const { data: bucketInfo, error: infoError } = await getBucketInfo( + forksOf, + { config } + ); + + if (infoError) { + printFailure(context, infoError.message); + process.exit(1); + } + + if (!bucketInfo.hasForks) { + printEmpty(context); + return; + } + + const forks: Array<{ name: string; created: Date }> = []; + + for (const bucket of data.buckets) { + if (bucket.name === forksOf) continue; + const { data: info } = await getBucketInfo(bucket.name, { config }); + if (info?.sourceBucketName === forksOf) { + 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 }); + return; + } + const buckets = data.buckets.map((bucket) => ({ name: bucket.name, created: bucket.creationDate, diff --git a/src/lib/mk.ts b/src/lib/mk.ts index 19abeb4..b9bee3e 100644 --- a/src/lib/mk.ts +++ b/src/lib/mk.ts @@ -40,6 +40,12 @@ export default async function mk(options: Record) { 'T', ]); let locations = getOption(options, ['locations', 'l', 'L']); + const forkOf = getOption(options, ['fork-of', 'forkOf', 'fork']); + const sourceSnapshot = getOption(options, [ + 'source-snapshot', + 'sourceSnapshot', + 'source-snap', + ]); // Handle deprecated --region and --consistency options const deprecatedRegion = getOption(options, ['region', 'r', 'R']); @@ -62,11 +68,18 @@ export default async function mk(options: Record) { ); } + if (sourceSnapshot && !forkOf) { + console.error('--source-snapshot requires --fork-of'); + process.exit(1); + } + const { error } = await createBucket(bucket, { defaultTier: (defaultTier ?? 'STANDARD') as StorageClass, enableSnapshot: enableSnapshots === true, access: (access ?? 'private') as 'public' | 'private', locations: parseLocations(locations ?? 'global'), + ...(forkOf ? { sourceBucketName: forkOf } : {}), + ...(sourceSnapshot ? { sourceBucketSnapshot: sourceSnapshot } : {}), config, }); diff --git a/src/specs.yaml b/src/specs.yaml index 4a96fe6..cedd2c4 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -262,6 +262,8 @@ commands: - "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" messages: onFailure: 'Failed to create. Check the name is valid and your credentials have write access' arguments: @@ -307,6 +309,12 @@ commands: alias: l options: *location_options default: 'global' + - name: fork-of + description: Create this bucket as a fork (copy-on-write clone) of the named source bucket + alias: fork + - name: source-snapshot + description: 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 + alias: source-snap # touch - name: touch @@ -578,6 +586,7 @@ commands: examples: - "tigris buckets list" - "tigris buckets list --format json" + - "tigris buckets list --forks-of my-bucket" messages: onStart: 'Listing buckets...' onSuccess: 'Found {{count}} bucket(s)' @@ -589,6 +598,8 @@ commands: alias: f options: [json, table, xml] default: table + - name: forks-of + description: Only list buckets that are forks of the named source bucket # create - name: create description: Create a new bucket with optional access, tier, and location settings @@ -597,6 +608,8 @@ commands: - "tigris buckets create my-bucket" - "tigris buckets create my-bucket --access public --locations iad" - "tigris buckets create my-bucket --enable-snapshots --default-tier STANDARD_IA" + - "tigris buckets create my-fork --fork-of my-bucket" + - "tigris buckets create my-fork --fork-of my-bucket --source-snapshot 1765889000501544464" messages: onStart: 'Creating bucket...' onSuccess: "Bucket '{{name}}' created successfully" @@ -641,6 +654,12 @@ commands: alias: l options: *location_options default: 'global' + - name: fork-of + description: Create this bucket as a fork (copy-on-write clone) of the named source bucket + alias: fork + - name: source-snapshot + description: 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 + alias: source-snap # get - name: get description: Show details for a bucket including access level, region, tier, and custom domain @@ -935,7 +954,7 @@ commands: # Manage forks ######################### - name: forks - description: List and create forks. A fork is a writable copy-on-write clone of a bucket, useful for testing or branching data + description: (Deprecated, use "buckets create --fork-of" and "buckets list --forks-of") List and create forks alias: f examples: - "tigris forks list my-bucket" @@ -943,7 +962,8 @@ commands: commands: # list - name: list - description: List all forks created from the given source bucket + 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" @@ -953,6 +973,7 @@ commands: 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 @@ -967,15 +988,17 @@ commands: default: table # create - name: create - description: Create a new fork (copy-on-write clone) of the source bucket. Optionally fork from a specific snapshot + 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 snap-2025-01-01" + - "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 @@ -990,7 +1013,7 @@ commands: examples: - my-fork - name: snapshot - description: Create fork from a specific snapshot + description: Create fork from a specific snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464) alias: s ######################### diff --git a/src/types.ts b/src/types.ts index 040851b..adb0efa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,7 @@ export interface Messages { onFailure?: string; onEmpty?: string; onAlreadyDone?: string; + onDeprecated?: string; hint?: string; } @@ -31,6 +32,7 @@ export interface CommandSpec { examples?: string[]; commands?: CommandSpec[]; // recursive - can nest infinitely default?: string; + deprecated?: boolean; message?: string; messages?: Messages; } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 88039ba..196c7cd 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -150,6 +150,15 @@ export function printHint( } } +/** + * Print a deprecation warning for a command + * Suppressed when output is piped/redirected + */ +export function printDeprecated(message: string): void { + if (!isTTY()) return; + console.warn(`⚠ Deprecated: ${message}`); +} + /** * Helper to create a message context */ From 676d78e330b72bb4bed7dc9c81a993189fd71c83 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Mon, 16 Mar 2026 15:34:23 +0100 Subject: [PATCH 5/7] feat: add --json and --yes for agents (#39) * test: add more tests * test: enhances integration test suite * feat: implement global --json * fix: confirmation on destructive commands * feat: add global --yes flag * chore: pr comments Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/ci.yaml | 44 + .github/workflows/pr.yaml | 25 - .github/workflows/release.yaml | 5 +- package.json | 4 +- src/cli-core.ts | 1 + src/lib/access-keys/assign.ts | 13 + src/lib/access-keys/create.ts | 30 +- src/lib/access-keys/delete.ts | 20 + src/lib/access-keys/get.ts | 31 +- src/lib/access-keys/list.ts | 10 +- src/lib/buckets/create.ts | 14 + src/lib/buckets/delete.ts | 31 +- src/lib/buckets/get.ts | 5 +- src/lib/buckets/list.ts | 5 +- src/lib/buckets/set-locations.ts | 2 + src/lib/buckets/set.ts | 9 + src/lib/configure/index.ts | 3 + src/lib/cp.ts | 184 +++- src/lib/credentials/test.ts | 31 +- src/lib/forks/list.ts | 5 +- src/lib/iam/policies/delete.ts | 13 + src/lib/iam/policies/edit.ts | 3 + src/lib/iam/policies/get.ts | 5 +- src/lib/iam/policies/list.ts | 5 +- src/lib/iam/users/list.ts | 5 +- src/lib/iam/users/remove.ts | 13 + src/lib/iam/users/revoke-invitation.ts | 15 + src/lib/iam/users/update-role.ts | 3 + src/lib/login/credentials.ts | 3 + src/lib/login/select.ts | 5 + src/lib/ls.ts | 8 +- src/lib/mk.ts | 25 +- src/lib/mv.ts | 67 +- src/lib/objects/delete.ts | 35 +- src/lib/objects/get.ts | 15 + src/lib/objects/list.ts | 5 +- src/lib/objects/put.ts | 5 +- src/lib/objects/set.ts | 17 + src/lib/organizations/list.ts | 8 +- src/lib/presign.ts | 5 +- src/lib/rm.ts | 66 +- src/lib/snapshots/list.ts | 5 +- src/lib/stat.ts | 5 +- src/lib/touch.ts | 11 +- src/lib/whoami.ts | 32 +- src/specs.yaml | 230 ++++- src/utils/interactive.ts | 33 + src/utils/locations.ts | 3 + test/cli-core.test.ts | 357 +++++++ test/cli.test.ts | 1236 +++++++++++++++++++++++- test/specs-completeness.test.ts | 163 ++++ test/utils/format.test.ts | 162 ++++ test/utils/locations.test.ts | 60 ++ test/utils/messages.test.ts | 171 ++++ test/utils/specs.test.ts | 9 +- 55 files changed, 3082 insertions(+), 193 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 .github/workflows/pr.yaml create mode 100644 src/utils/interactive.ts create mode 100644 test/cli-core.test.ts create mode 100644 test/specs-completeness.test.ts create mode 100644 test/utils/format.test.ts create mode 100644 test/utils/locations.test.ts create mode 100644 test/utils/messages.test.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7bcfae5 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,44 @@ +name: CI + +on: + pull_request: + branches: + - main + - next + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm ci + - run: npm run test + - run: npm run build + + - uses: oven-sh/setup-bun@v2 + - run: bun run build:binary + + integration: + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm ci + - run: npm run build + - run: npm run test:integration + env: + TIGRIS_STORAGE_ACCESS_KEY_ID: ${{ secrets.TIGRIS_STORAGE_ACCESS_KEY_ID }} + TIGRIS_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.TIGRIS_STORAGE_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml deleted file mode 100644 index f4be17e..0000000 --- a/.github/workflows/pr.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Pull Request - -on: - pull_request: - branches: - - main - - next - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: '22' - - run: npm ci - - run: npm run test - - - run: npm run build - - - uses: oven-sh/setup-bun@v2 - - run: bun run build:binary diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9aff9f5..29a2208 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,7 +34,10 @@ jobs: - run: npm ci - run: npm run lint - run: npm run build - - run: npm run test + - run: npm run test:all + env: + TIGRIS_STORAGE_ACCESS_KEY_ID: ${{ secrets.TIGRIS_STORAGE_ACCESS_KEY_ID }} + TIGRIS_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.TIGRIS_STORAGE_SECRET_ACCESS_KEY }} - run: npm run publint - run: npm audit signatures diff --git a/package.json b/package.json index 173ef89..31860c8 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "lint:fix": "eslint src --fix", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", - "test": "vitest run", + "test": "vitest run test/utils test/cli-core.test.ts test/specs-completeness.test.ts", "test:watch": "vitest", - "test:unit": "vitest run test/utils", + "test:all": "vitest run", "test:integration": "vitest run test/cli.test.ts", "publint": "publint", "updatedocs": "tsx scripts/update-docs.ts", diff --git a/src/cli-core.ts b/src/cli-core.ts index fff3d63..a19d932 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -515,6 +515,7 @@ export function createProgram(config: CLIConfig): CommanderCommand { const program = new CommanderCommand(); program.name(specs.name).description(specs.description).version(version); + program.option('-y, --yes', 'Skip all confirmation prompts'); registerCommands(config, program, specs.commands); diff --git a/src/lib/access-keys/assign.ts b/src/lib/access-keys/assign.ts index a7cf714..5b388e3 100644 --- a/src/lib/access-keys/assign.ts +++ b/src/lib/access-keys/assign.ts @@ -24,6 +24,11 @@ function normalizeToArray(value: T | T[] | undefined): T[] { export default async function assign(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const id = getOption(options, ['id']); const admin = getOption(options, ['admin']); const revokeRoles = getOption(options, [ @@ -83,6 +88,10 @@ export default async function assign(options: Record) { process.exit(1); } + if (format === 'json') { + console.log(JSON.stringify({ action: 'revoked', id })); + } + printSuccess(context); return; } @@ -149,5 +158,9 @@ export default async function assign(options: Record) { process.exit(1); } + if (format === 'json') { + console.log(JSON.stringify({ action: 'assigned', id, assignments })); + } + printSuccess(context); } diff --git a/src/lib/access-keys/create.ts b/src/lib/access-keys/create.ts index df86eaf..2dbe5fe 100644 --- a/src/lib/access-keys/create.ts +++ b/src/lib/access-keys/create.ts @@ -16,6 +16,11 @@ const context = msg('access-keys', 'create'); export default async function create(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const name = getOption(options, ['name']); if (!name) { @@ -58,13 +63,24 @@ export default async function create(options: Record) { process.exit(1); } - console.log(` Name: ${data.name}`); - console.log(` Access Key ID: ${data.id}`); - console.log(` Secret Access Key: ${data.secret}`); - console.log(''); - console.log( - ' Save these credentials securely. The secret will not be shown again.' - ); + if (format === 'json') { + console.log( + JSON.stringify({ + action: 'created', + name: data.name, + id: data.id, + secret: data.secret, + }) + ); + } else { + console.log(` Name: ${data.name}`); + console.log(` Access Key ID: ${data.id}`); + console.log(` Secret Access Key: ${data.secret}`); + console.log(''); + console.log( + ' Save these credentials securely. The secret will not be shown again.' + ); + } printSuccess(context); } diff --git a/src/lib/access-keys/delete.ts b/src/lib/access-keys/delete.ts index 5898aca..8143ba9 100644 --- a/src/lib/access-keys/delete.ts +++ b/src/lib/access-keys/delete.ts @@ -10,13 +10,20 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { requireInteractive, confirm } from '../../utils/interactive.js'; const context = msg('access-keys', 'delete'); export default async function remove(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const id = getOption(options, ['id']); + const force = getOption(options, ['force', 'yes', 'y']); if (!id) { printFailure(context, 'Access key ID is required'); @@ -41,6 +48,15 @@ export default async function remove(options: Record) { process.exit(1); } + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm(`Delete access key '${id}'?`); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + const accessToken = await authClient.getAccessToken(); const selectedOrg = getSelectedOrganization(); const tigrisConfig = getTigrisConfig(); @@ -58,5 +74,9 @@ export default async function remove(options: Record) { process.exit(1); } + if (format === 'json') { + console.log(JSON.stringify({ action: 'deleted', id })); + } + printSuccess(context); } diff --git a/src/lib/access-keys/get.ts b/src/lib/access-keys/get.ts index ee5e81d..23c9129 100644 --- a/src/lib/access-keys/get.ts +++ b/src/lib/access-keys/get.ts @@ -16,6 +16,11 @@ const context = msg('access-keys', 'get'); export default async function get(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const id = getOption(options, ['id']); if (!id) { @@ -58,19 +63,23 @@ export default async function get(options: Record) { process.exit(1); } - console.log(` Name: ${data.name}`); - console.log(` ID: ${data.id}`); - console.log(` Status: ${data.status}`); - console.log(` Created: ${data.createdAt}`); - console.log(` Organization: ${data.organizationId}`); + if (format === 'json') { + console.log(JSON.stringify(data)); + } else { + console.log(` Name: ${data.name}`); + console.log(` ID: ${data.id}`); + console.log(` Status: ${data.status}`); + console.log(` Created: ${data.createdAt}`); + console.log(` Organization: ${data.organizationId}`); - if (data.roles && data.roles.length > 0) { - console.log(` Roles:`); - for (const role of data.roles) { - console.log(` - ${role.bucket}: ${role.role}`); + if (data.roles && data.roles.length > 0) { + console.log(` Roles:`); + for (const role of data.roles) { + console.log(` - ${role.bucket}: ${role.role}`); + } + } else { + console.log(` Roles: None`); } - } else { - console.log(` Roles: None`); } printSuccess(context); diff --git a/src/lib/access-keys/list.ts b/src/lib/access-keys/list.ts index 6c2e740..ceddbb5 100644 --- a/src/lib/access-keys/list.ts +++ b/src/lib/access-keys/list.ts @@ -1,4 +1,5 @@ import { formatOutput } from '../../utils/format.js'; +import { getOption } from '../../utils/options.js'; import { getLoginMethod } from '../../auth/s3-client.js'; import { getAuthClient } from '../../auth/client.js'; import { getSelectedOrganization } from '../../auth/storage.js'; @@ -14,9 +15,14 @@ import { const context = msg('access-keys', 'list'); -export default async function list() { +export default async function list(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const loginMethod = await getLoginMethod(); if (loginMethod !== 'oauth') { @@ -64,7 +70,7 @@ export default async function list() { created: key.createdAt, })); - const output = formatOutput(keys, 'table', 'keys', 'key', [ + const output = formatOutput(keys, format!, 'keys', 'key', [ { key: 'name', header: 'Name' }, { key: 'id', header: 'ID' }, { key: 'status', header: 'Status' }, diff --git a/src/lib/buckets/create.ts b/src/lib/buckets/create.ts index c63e8f9..f625e6f 100644 --- a/src/lib/buckets/create.ts +++ b/src/lib/buckets/create.ts @@ -1,5 +1,6 @@ import { getOption } from '../../utils/options'; import enquirer from 'enquirer'; +import { requireInteractive } from '../../utils/interactive.js'; import { getArgumentSpec, buildPromptChoices } from '../../utils/specs.js'; import { StorageClass, createBucket } from '@tigrisdata/storage'; import { getStorageConfig } from '../../auth/s3-client'; @@ -19,6 +20,11 @@ const context = msg('buckets', 'create'); export default async function create(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + let name = getOption(options, ['name']); const isPublic = getOption(options, ['public']); let access = isPublic @@ -65,6 +71,8 @@ export default async function create(options: Record) { let parsedLocations: BucketLocations | undefined; if (interactive) { + requireInteractive('Provide --name to skip interactive mode'); + const accessSpec = getArgumentSpec('buckets', 'access', 'create'); const accessChoices = buildPromptChoices(accessSpec!); const accessDefault = accessChoices?.findIndex( @@ -151,5 +159,11 @@ export default async function create(options: Record) { process.exit(1); } + if (format === 'json') { + console.log( + JSON.stringify({ action: 'created', name, ...(forkOf ? { forkOf } : {}) }) + ); + } + printSuccess(context, { name }); } diff --git a/src/lib/buckets/delete.ts b/src/lib/buckets/delete.ts index 94a7af9..7cc6520 100644 --- a/src/lib/buckets/delete.ts +++ b/src/lib/buckets/delete.ts @@ -7,13 +7,20 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { requireInteractive, confirm } from '../../utils/interactive.js'; const context = msg('buckets', 'delete'); export default async function deleteBucket(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const names = getOption(options, ['name']); + const force = getOption(options, ['force', 'yes', 'y']); if (!names) { printFailure(context, 'Bucket name is required'); @@ -23,14 +30,34 @@ export default async function deleteBucket(options: Record) { const bucketNames = Array.isArray(names) ? names : [names]; const config = await getStorageConfig(); + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm(`Delete ${bucketNames.length} bucket(s)?`); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + + const deleted: string[] = []; + const errors: { name: string; error: string }[] = []; for (const name of bucketNames) { const { error } = await removeBucket(name, { config }); if (error) { printFailure(context, error.message, { name }); - process.exit(1); + errors.push({ name, error: error.message }); + } else { + deleted.push(name); + printSuccess(context, { name }); } + } + + if (format === 'json') { + console.log(JSON.stringify({ action: 'deleted', names: deleted, errors })); + } - printSuccess(context, { name }); + if (errors.length > 0) { + process.exit(1); } } diff --git a/src/lib/buckets/get.ts b/src/lib/buckets/get.ts index 9462168..48844e3 100644 --- a/src/lib/buckets/get.ts +++ b/src/lib/buckets/get.ts @@ -16,7 +16,10 @@ export default async function get(options: Record) { printStart(context); const name = getOption(options, ['name']); - const format = getOption(options, ['format']) || 'table'; + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F']) || 'table'; if (!name) { printFailure(context, 'Bucket name is required'); diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index 02387b8..8391a6b 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -16,7 +16,10 @@ export default async function list(options: Record) { printStart(context); try { - const format = getOption(options, ['format', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const forksOf = getOption(options, ['forks-of', 'forksOf']); const config = await getStorageConfig(); diff --git a/src/lib/buckets/set-locations.ts b/src/lib/buckets/set-locations.ts index 4b03ef3..6720032 100644 --- a/src/lib/buckets/set-locations.ts +++ b/src/lib/buckets/set-locations.ts @@ -4,6 +4,7 @@ import { getSelectedOrganization } from '../../auth/storage.js'; import { updateBucket } from '@tigrisdata/storage'; import type { BucketLocations } from '@tigrisdata/storage'; import { parseLocations, promptLocations } from '../../utils/locations.js'; +import { requireInteractive } from '../../utils/interactive.js'; import { printStart, printSuccess, @@ -28,6 +29,7 @@ export default async function setLocations(options: Record) { if (locations !== undefined) { parsedLocations = parseLocations(locations); } else { + requireInteractive('Provide --locations flag'); try { parsedLocations = await promptLocations(); } catch (err) { diff --git a/src/lib/buckets/set.ts b/src/lib/buckets/set.ts index e9279df..2d70f3a 100644 --- a/src/lib/buckets/set.ts +++ b/src/lib/buckets/set.ts @@ -15,6 +15,11 @@ const context = msg('buckets', 'set'); export default async function set(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format'], 'table'); + const name = getOption(options, ['name']); const access = getOption(options, ['access']); let locations = getOption(options, ['locations']); @@ -134,5 +139,9 @@ export default async function set(options: Record) { process.exit(1); } + if (format === 'json') { + console.log(JSON.stringify({ action: 'updated', name })); + } + printSuccess(context, { name }); } diff --git a/src/lib/configure/index.ts b/src/lib/configure/index.ts index e4b65db..0795a6d 100644 --- a/src/lib/configure/index.ts +++ b/src/lib/configure/index.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive } from '../../utils/interactive.js'; import { storeCredentials, storeLoginMethod } from '../../auth/storage.js'; import { DEFAULT_STORAGE_ENDPOINT } from '../../constants.js'; import { @@ -61,6 +62,8 @@ export default async function configure(options: Record) { }); } + requireInteractive('Provide --access-key, --access-secret, and --endpoint'); + const responses = await prompt<{ accessKey?: string; accessSecret?: string; diff --git a/src/lib/cp.ts b/src/lib/cp.ts index eebd236..7937657 100644 --- a/src/lib/cp.ts +++ b/src/lib/cp.ts @@ -26,6 +26,8 @@ import { executeWithConcurrency } from '../utils/concurrency.js'; import { calculateUploadParams } from '../utils/upload.js'; import type { ParsedPath } from '../types.js'; +let _jsonMode = false; + type CopyDirection = 'local-to-remote' | 'remote-to-local' | 'remote-to-remote'; function detectDirection(src: string, dest: string): CopyDirection { @@ -287,7 +289,17 @@ async function copyLocalToRemote( if (isWildcard) { const files = listLocalFilesWithWildcard(localPath, recursive); if (files.length === 0) { - console.log('No files matching pattern'); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'local-to-remote', + count: 0, + }) + ); + } else { + console.log('No files matching pattern'); + } return; } @@ -303,13 +315,26 @@ async function copyLocalToRemote( console.error(`Failed to upload ${file}: ${result.error}`); return false; } else { - console.log(`Uploaded ${file} -> t3://${destParsed.bucket}/${destKey}`); + if (!_jsonMode) + console.log( + `Uploaded ${file} -> t3://${destParsed.bucket}/${destKey}` + ); return true; } }); const results = await executeWithConcurrency(tasks, 8); const copied = results.filter(Boolean).length; - console.log(`Uploaded ${copied} file(s)`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'local-to-remote', + count: copied, + }) + ); + } else { + console.log(`Uploaded ${copied} file(s)`); + } return; } @@ -331,7 +356,17 @@ async function copyLocalToRemote( const files = listLocalFiles(localPath); if (files.length === 0) { - console.log('No files to upload'); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'local-to-remote', + count: 0, + }) + ); + } else { + console.log('No files to upload'); + } return; } @@ -352,13 +387,26 @@ async function copyLocalToRemote( console.error(`Failed to upload ${file}: ${result.error}`); return false; } else { - console.log(`Uploaded ${file} -> t3://${destParsed.bucket}/${destKey}`); + if (!_jsonMode) + console.log( + `Uploaded ${file} -> t3://${destParsed.bucket}/${destKey}` + ); return true; } }); const dirResults = await executeWithConcurrency(dirTasks, 8); const copied = dirResults.filter(Boolean).length; - console.log(`Uploaded ${copied} file(s)`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'local-to-remote', + count: copied, + }) + ); + } else { + console.log(`Uploaded ${copied} file(s)`); + } } else { // Single file const fileName = basename(localPath); @@ -387,13 +435,25 @@ async function copyLocalToRemote( destParsed.bucket, destKey, config, - true + !_jsonMode ); if (result.error) { console.error(result.error); process.exit(1); } - console.log(`Uploaded ${src} -> t3://${destParsed.bucket}/${destKey}`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'local-to-remote', + count: 1, + src, + dest: `t3://${destParsed.bucket}/${destKey}`, + }) + ); + } else { + console.log(`Uploaded ${src} -> t3://${destParsed.bucket}/${destKey}`); + } } } @@ -468,7 +528,17 @@ async function copyRemoteToLocal( } if (filesToDownload.length === 0) { - console.log('No objects to download'); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-local', + count: 0, + }) + ); + } else { + console.log('No objects to download'); + } return; } @@ -488,15 +558,26 @@ async function copyRemoteToLocal( console.error(`Failed to download ${item.name}: ${result.error}`); return false; } else { - console.log( - `Downloaded t3://${srcParsed.bucket}/${item.name} -> ${localFilePath}` - ); + if (!_jsonMode) + console.log( + `Downloaded t3://${srcParsed.bucket}/${item.name} -> ${localFilePath}` + ); return true; } }); const downloadResults = await executeWithConcurrency(downloadTasks, 8); const downloaded = downloadResults.filter(Boolean).length; - console.log(`Downloaded ${downloaded} file(s)`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-local', + count: downloaded, + }) + ); + } else { + console.log(`Downloaded ${downloaded} file(s)`); + } } else { // Single object const srcFileName = srcParsed.path.split('/').pop()!; @@ -521,15 +602,27 @@ async function copyRemoteToLocal( srcParsed.path, localFilePath, config, - true + !_jsonMode ); if (result.error) { console.error(result.error); process.exit(1); } - console.log( - `Downloaded t3://${srcParsed.bucket}/${srcParsed.path} -> ${localFilePath}` - ); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-local', + count: 1, + src: `t3://${srcParsed.bucket}/${srcParsed.path}`, + dest: localFilePath, + }) + ); + } else { + console.log( + `Downloaded t3://${srcParsed.bucket}/${srcParsed.path} -> ${localFilePath}` + ); + } } } @@ -636,9 +729,10 @@ async function copyRemoteToRemote( console.error(`Failed to copy ${item.name}: ${copyResult.error}`); return false; } else { - console.log( - `Copied t3://${srcParsed.bucket}/${item.name} -> t3://${destParsed.bucket}/${destKey}` - ); + if (!_jsonMode) + console.log( + `Copied t3://${srcParsed.bucket}/${item.name} -> t3://${destParsed.bucket}/${destKey}` + ); return true; } }); @@ -679,11 +773,31 @@ async function copyRemoteToRemote( } if (copied === 0) { - console.log('No objects to copy'); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-remote', + count: 0, + }) + ); + } else { + console.log('No objects to copy'); + } return; } - console.log(`Copied ${copied} object(s)`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-remote', + count: copied, + }) + ); + } else { + console.log(`Copied ${copied} object(s)`); + } } else { // Single object const srcFileName = srcParsed.path.split('/').pop()!; @@ -717,7 +831,7 @@ async function copyRemoteToRemote( srcParsed.path, destParsed.bucket, destKey, - true + !_jsonMode ); if (result.error) { @@ -725,9 +839,21 @@ async function copyRemoteToRemote( process.exit(1); } - console.log( - `Copied t3://${srcParsed.bucket}/${srcParsed.path} -> t3://${destParsed.bucket}/${destKey}` - ); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'copied', + direction: 'remote-to-remote', + count: 1, + src: `t3://${srcParsed.bucket}/${srcParsed.path}`, + dest: `t3://${destParsed.bucket}/${destKey}`, + }) + ); + } else { + console.log( + `Copied t3://${srcParsed.bucket}/${srcParsed.path} -> t3://${destParsed.bucket}/${destKey}` + ); + } } } @@ -741,6 +867,12 @@ export default async function cp(options: Record) { } const recursive = !!getOption(options, ['recursive', 'r']); + const jsonFlag = getOption(options, ['json']); + const format = jsonFlag + ? 'json' + : getOption(options, ['format'], 'table'); + _jsonMode = format === 'json'; + const direction = detectDirection(src, dest); const config = await getStorageConfig({ withCredentialProvider: true }); diff --git a/src/lib/credentials/test.ts b/src/lib/credentials/test.ts index 593d07d..99443d2 100644 --- a/src/lib/credentials/test.ts +++ b/src/lib/credentials/test.ts @@ -14,6 +14,11 @@ const context = msg('credentials', 'test'); export default async function test(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const bucket = getOption(options, ['bucket', 'b']); const config = await getStorageConfig(); @@ -49,10 +54,20 @@ export default async function test(options: Record) { process.exit(1); } - console.log(` Bucket: ${bucket}`); - console.log(` Access verified.`); - if (data.sourceBucketName) { - console.log(` Fork of: ${data.sourceBucketName}`); + if (format === 'json') { + console.log( + JSON.stringify({ + valid: true, + bucket, + ...(data.sourceBucketName ? { forkOf: data.sourceBucketName } : {}), + }) + ); + } else { + console.log(` Bucket: ${bucket}`); + console.log(` Access verified.`); + if (data.sourceBucketName) { + console.log(` Fork of: ${data.sourceBucketName}`); + } } } else { // Test general access by listing buckets @@ -63,7 +78,13 @@ export default async function test(options: Record) { process.exit(1); } - console.log(` Access verified. Found ${data.buckets.length} bucket(s).`); + if (format === 'json') { + console.log( + JSON.stringify({ valid: true, bucketCount: data.buckets.length }) + ); + } else { + console.log(` Access verified. Found ${data.buckets.length} bucket(s).`); + } } printSuccess(context); diff --git a/src/lib/forks/list.ts b/src/lib/forks/list.ts index 1348754..9d13c4d 100644 --- a/src/lib/forks/list.ts +++ b/src/lib/forks/list.ts @@ -16,7 +16,10 @@ export default async function list(options: Record) { printStart(context); const name = getOption(options, ['name']); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); if (!name) { printFailure(context, 'Source bucket name is required'); diff --git a/src/lib/iam/policies/delete.ts b/src/lib/iam/policies/delete.ts index f8fc766..acfa464 100644 --- a/src/lib/iam/policies/delete.ts +++ b/src/lib/iam/policies/delete.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive, confirm } from '../../../utils/interactive.js'; import { getOption } from '../../../utils/options.js'; import { getLoginMethod } from '../../../auth/s3-client.js'; import { getAuthClient } from '../../../auth/client.js'; @@ -20,6 +21,7 @@ export default async function del(options: Record) { printStart(context); let resource = getOption(options, ['resource']); + const force = getOption(options, ['force', 'yes', 'y']); const loginMethod = await getLoginMethod(); @@ -65,6 +67,8 @@ export default async function del(options: Record) { return; } + requireInteractive('Provide the policy ARN as a positional argument'); + const { selected } = await prompt<{ selected: string }>({ type: 'select', name: 'selected', @@ -78,6 +82,15 @@ export default async function del(options: Record) { resource = selected; } + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm(`Delete policy '${resource}'?`); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + const { error } = await deletePolicy(resource, { config: iamConfig, }); diff --git a/src/lib/iam/policies/edit.ts b/src/lib/iam/policies/edit.ts index b36d6d5..3cb7cd4 100644 --- a/src/lib/iam/policies/edit.ts +++ b/src/lib/iam/policies/edit.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync } from 'node:fs'; import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive } from '../../../utils/interactive.js'; import { getOption } from '../../../utils/options.js'; import { getLoginMethod } from '../../../auth/s3-client.js'; import { getAuthClient } from '../../../auth/client.js'; @@ -83,6 +84,8 @@ export default async function edit(options: Record) { return; } + requireInteractive('Provide the policy ARN as a positional argument'); + const { selected } = await prompt<{ selected: string }>({ type: 'select', name: 'selected', diff --git a/src/lib/iam/policies/get.ts b/src/lib/iam/policies/get.ts index a88a86f..c731ff0 100644 --- a/src/lib/iam/policies/get.ts +++ b/src/lib/iam/policies/get.ts @@ -21,7 +21,10 @@ export default async function get(options: Record) { printStart(context); let resource = getOption(options, ['resource']); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const loginMethod = await getLoginMethod(); diff --git a/src/lib/iam/policies/list.ts b/src/lib/iam/policies/list.ts index da5afed..aa1cf6a 100644 --- a/src/lib/iam/policies/list.ts +++ b/src/lib/iam/policies/list.ts @@ -18,7 +18,10 @@ const context = msg('iam policies', 'list'); export default async function list(options: Record) { printStart(context); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const loginMethod = await getLoginMethod(); diff --git a/src/lib/iam/users/list.ts b/src/lib/iam/users/list.ts index 653116c..81a35cc 100644 --- a/src/lib/iam/users/list.ts +++ b/src/lib/iam/users/list.ts @@ -24,7 +24,10 @@ const context = msg('iam users', 'list'); export default async function list(options: Record) { printStart(context); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const loginMethod = await getLoginMethod(); diff --git a/src/lib/iam/users/remove.ts b/src/lib/iam/users/remove.ts index 5450eff..6e1cac3 100644 --- a/src/lib/iam/users/remove.ts +++ b/src/lib/iam/users/remove.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive, confirm } from '../../../utils/interactive.js'; import { getOption } from '../../../utils/options.js'; import { getLoginMethod } from '../../../auth/s3-client.js'; import { getAuthClient } from '../../../auth/client.js'; @@ -21,6 +22,7 @@ export default async function removeUser(options: Record) { printStart(context); const resourceOption = getOption(options, ['resource']); + const force = getOption(options, ['force', 'yes', 'y']); const loginMethod = await getLoginMethod(); @@ -83,6 +85,8 @@ export default async function removeUser(options: Record) { return; } + requireInteractive('Provide user ID(s) as a positional argument'); + const { selected } = await prompt<{ selected: string[] }>({ type: 'multiselect', name: 'selected', @@ -96,6 +100,15 @@ export default async function removeUser(options: Record) { resources = selected; } + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm(`Remove ${resources.length} user(s)?`); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + const { error } = await removeUserFromOrg(resources, { config: iamConfig, }); diff --git a/src/lib/iam/users/revoke-invitation.ts b/src/lib/iam/users/revoke-invitation.ts index a1c78d8..cabd1ea 100644 --- a/src/lib/iam/users/revoke-invitation.ts +++ b/src/lib/iam/users/revoke-invitation.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive, confirm } from '../../../utils/interactive.js'; import { getOption } from '../../../utils/options.js'; import { getLoginMethod } from '../../../auth/s3-client.js'; import { getAuthClient } from '../../../auth/client.js'; @@ -23,6 +24,7 @@ export default async function revokeInvitation( printStart(context); const resourceOption = getOption(options, ['resource']); + const force = getOption(options, ['force', 'yes', 'y']); const loginMethod = await getLoginMethod(); @@ -85,6 +87,8 @@ export default async function revokeInvitation( return; } + requireInteractive('Provide invitation ID(s) as a positional argument'); + const { selected } = await prompt<{ selected: string[] }>({ type: 'multiselect', name: 'selected', @@ -99,6 +103,17 @@ export default async function revokeInvitation( resources = selected; } + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm( + `Revoke ${resources.length} invitation(s)?` + ); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + const { error } = await revokeInv(resources, { config: iamConfig, }); diff --git a/src/lib/iam/users/update-role.ts b/src/lib/iam/users/update-role.ts index 30b1ea4..52e6ef6 100644 --- a/src/lib/iam/users/update-role.ts +++ b/src/lib/iam/users/update-role.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive } from '../../../utils/interactive.js'; import { getOption } from '../../../utils/options.js'; import { getLoginMethod } from '../../../auth/s3-client.js'; import { getAuthClient } from '../../../auth/client.js'; @@ -108,6 +109,8 @@ export default async function updateRole(options: Record) { return; } + requireInteractive('Provide user ID(s) as a positional argument'); + const { selected } = await prompt<{ selected: string[] }>({ type: 'multiselect', name: 'selected', diff --git a/src/lib/login/credentials.ts b/src/lib/login/credentials.ts index 3c7cb89..35b5800 100644 --- a/src/lib/login/credentials.ts +++ b/src/lib/login/credentials.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive } from '../../utils/interactive.js'; import { getSavedCredentials, storeLoginMethod, @@ -58,6 +59,8 @@ export default async function credentials(options: Record) { }); } + requireInteractive('Provide --access-key and --access-secret'); + const responses = await prompt<{ accessKey?: string; accessSecret?: string; diff --git a/src/lib/login/select.ts b/src/lib/login/select.ts index a765c72..bc2ca7b 100644 --- a/src/lib/login/select.ts +++ b/src/lib/login/select.ts @@ -1,5 +1,6 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; +import { requireInteractive } from '../../utils/interactive.js'; import { oauth } from './oauth.js'; import credentials from './credentials.js'; @@ -30,6 +31,10 @@ export default async function select(options: Record) { } // Prompt user to choose login method + requireInteractive( + 'Use "tigris login oauth" or "tigris login credentials --access-key ... --access-secret ..."' + ); + const { method } = await prompt<{ method: string }>({ type: 'select', name: 'method', diff --git a/src/lib/ls.ts b/src/lib/ls.ts index a7682ce..eed18d7 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -11,6 +11,10 @@ export default async function ls(options: Record) { 'snapshotVersion', 'snapshot', ]); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); if (!pathString) { // No path provided, list all buckets @@ -27,7 +31,7 @@ export default async function ls(options: Record) { created: bucket.creationDate, })); - const output = formatOutput(buckets, 'table', 'buckets', 'bucket', [ + const output = formatOutput(buckets, format!, 'buckets', 'bucket', [ { key: 'name', header: 'Name' }, { key: 'created', header: 'Created' }, ]); @@ -85,7 +89,7 @@ export default async function ls(options: Record) { item.key !== '' && arr.findIndex((i) => i.key === item.key) === index ); - const output = formatOutput(objects, 'table', 'objects', 'object', [ + const output = formatOutput(objects, format!, 'objects', 'object', [ { key: 'key', header: 'Key' }, { key: 'size', header: 'Size' }, { key: 'modified', header: 'Modified' }, diff --git a/src/lib/mk.ts b/src/lib/mk.ts index b9bee3e..cf6024e 100644 --- a/src/lib/mk.ts +++ b/src/lib/mk.ts @@ -20,6 +20,10 @@ export default async function mk(options: Record) { } const config = await getStorageConfig(); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); if (!path) { // Create a bucket @@ -88,7 +92,13 @@ export default async function mk(options: Record) { process.exit(1); } - console.log(`Bucket '${bucket}' created`); + if (format === 'json') { + console.log( + JSON.stringify({ action: 'created', type: 'bucket', name: bucket }) + ); + } else { + console.log(`Bucket '${bucket}' created`); + } process.exit(0); } else { // Create a "folder" (empty object with trailing slash) @@ -106,7 +116,18 @@ export default async function mk(options: Record) { process.exit(1); } - console.log(`Folder '${bucket}/${folderPath}' created`); + if (format === 'json') { + console.log( + JSON.stringify({ + action: 'created', + type: 'folder', + bucket, + path: folderPath, + }) + ); + } else { + console.log(`Folder '${bucket}/${folderPath}' created`); + } process.exit(0); } } diff --git a/src/lib/mv.ts b/src/lib/mv.ts index be7062c..c2b68c4 100644 --- a/src/lib/mv.ts +++ b/src/lib/mv.ts @@ -1,4 +1,3 @@ -import * as readline from 'readline'; import { isRemotePath, parseRemotePath, @@ -10,28 +9,22 @@ import { import { getOption } from '../utils/options.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { formatSize } from '../utils/format.js'; +import { requireInteractive, confirm } from '../utils/interactive.js'; import { get, put, remove, list, head } from '@tigrisdata/storage'; import { calculateUploadParams } from '../utils/upload.js'; -async function confirm(message: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question(`${message} (y/N): `, (answer) => { - rl.close(); - resolve(answer.toLowerCase() === 'y'); - }); - }); -} +let _jsonMode = false; export default async function mv(options: Record) { const src = getOption(options, ['src']); const dest = getOption(options, ['dest']); - const force = getOption(options, ['force', 'f', 'F']); + const force = getOption(options, ['force', 'f', 'F', 'yes', 'y']); const recursive = !!getOption(options, ['recursive', 'r']); + const jsonFlag = getOption(options, ['json']); + const format = jsonFlag + ? 'json' + : getOption(options, ['format'], 'table'); + _jsonMode = format === 'json'; if (!src || !dest) { console.error('both src and dest arguments are required'); @@ -156,17 +149,22 @@ export default async function mv(options: Record) { : false; if (itemsToMove.length === 0 && !hasFolderMarker) { - console.log('No objects to move'); + if (_jsonMode) { + console.log(JSON.stringify({ action: 'moved', count: 0 })); + } else { + console.log('No objects to move'); + } return; } const totalToMove = itemsToMove.length + (hasFolderMarker ? 1 : 0); if (!force) { + requireInteractive('Use --yes to skip confirmation'); const confirmed = await confirm( `Are you sure you want to move ${totalToMove} object(s)?` ); if (!confirmed) { - console.log('Aborted'); + if (!_jsonMode) console.log('Aborted'); return; } } @@ -189,9 +187,10 @@ export default async function mv(options: Record) { if (moveResult.error) { console.error(`Failed to move ${item.name}: ${moveResult.error}`); } else { - console.log( - `Moved t3://${srcPath.bucket}/${item.name} -> t3://${destPath.bucket}/${destKey}` - ); + if (!_jsonMode) + console.log( + `Moved t3://${srcPath.bucket}/${item.name} -> t3://${destPath.bucket}/${destKey}` + ); moved++; } } @@ -237,7 +236,11 @@ export default async function mv(options: Record) { moved = 1; } - console.log(`Moved ${moved} object(s)`); + if (_jsonMode) { + console.log(JSON.stringify({ action: 'moved', count: moved })); + } else { + console.log(`Moved ${moved} object(s)`); + } } else { // Move single object const srcFileName = srcPath.path.split('/').pop()!; @@ -270,11 +273,12 @@ export default async function mv(options: Record) { } if (!force) { + requireInteractive('Use --yes to skip confirmation'); const confirmed = await confirm( `Are you sure you want to move 't3://${srcPath.bucket}/${srcPath.path}'?` ); if (!confirmed) { - console.log('Aborted'); + if (!_jsonMode) console.log('Aborted'); return; } } @@ -285,7 +289,7 @@ export default async function mv(options: Record) { srcPath.path, destPath.bucket, destKey, - true // show progress for single file + !_jsonMode // show progress for single file (not in JSON mode) ); if (result.error) { @@ -293,9 +297,20 @@ export default async function mv(options: Record) { process.exit(1); } - console.log( - `Moved t3://${srcPath.bucket}/${srcPath.path} -> t3://${destPath.bucket}/${destKey}` - ); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'moved', + count: 1, + src: `t3://${srcPath.bucket}/${srcPath.path}`, + dest: `t3://${destPath.bucket}/${destKey}`, + }) + ); + } else { + console.log( + `Moved t3://${srcPath.bucket}/${srcPath.path} -> t3://${destPath.bucket}/${destKey}` + ); + } } process.exit(0); } diff --git a/src/lib/objects/delete.ts b/src/lib/objects/delete.ts index 9c743d9..4cec92f 100644 --- a/src/lib/objects/delete.ts +++ b/src/lib/objects/delete.ts @@ -7,14 +7,21 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { requireInteractive, confirm } from '../../utils/interactive.js'; const context = msg('objects', 'delete'); export default async function deleteObject(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const bucket = getOption(options, ['bucket']); const keys = getOption(options, ['key']); + const force = getOption(options, ['force', 'yes', 'y']); if (!bucket) { printFailure(context, 'Bucket name is required'); @@ -29,6 +36,19 @@ export default async function deleteObject(options: Record) { const config = await getStorageConfig(); const keyList = Array.isArray(keys) ? keys : [keys]; + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm( + `Delete ${keyList.length} object(s) from '${bucket}'?` + ); + 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: { @@ -39,9 +59,20 @@ export default async function deleteObject(options: Record) { if (error) { printFailure(context, error.message, { key }); - process.exit(1); + errors.push({ key, error: error.message }); + } else { + deleted.push(key); + printSuccess(context, { key }); } + } + + if (format === 'json') { + console.log( + JSON.stringify({ action: 'deleted', bucket, keys: deleted, errors }) + ); + } - printSuccess(context, { key }); + if (errors.length > 0) { + process.exit(1); } } diff --git a/src/lib/objects/get.ts b/src/lib/objects/get.ts index 7c7ae16..16560e0 100644 --- a/src/lib/objects/get.ts +++ b/src/lib/objects/get.ts @@ -108,6 +108,11 @@ function detectFormat(key: string, output?: string): 'string' | 'stream' { export default async function getObject(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const outputFormat = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const bucket = getOption(options, ['bucket']); const key = getOption(options, ['key']); const output = getOption(options, ['output', 'o', 'O']); @@ -151,6 +156,11 @@ export default async function getObject(options: Record) { const writeStream = createWriteStream(output); await pipeline(Readable.fromWeb(data as ReadableStream), writeStream); printSuccess(context, { key, output }); + if (outputFormat === 'json') { + console.log( + JSON.stringify({ action: 'downloaded', bucket, key, output }) + ); + } } else { // Stream to stdout for binary data await pipeline(Readable.fromWeb(data as ReadableStream), process.stdout); @@ -173,6 +183,11 @@ export default async function getObject(options: Record) { if (output) { writeFileSync(output, data); printSuccess(context, { key, output }); + if (outputFormat === 'json') { + console.log( + JSON.stringify({ action: 'downloaded', bucket, key, output }) + ); + } } else { console.log(data); printSuccess(context); diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index 5f83235..16dfdf6 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -17,7 +17,10 @@ export default async function listObjects(options: Record) { const bucket = getOption(options, ['bucket']); const prefix = getOption(options, ['prefix', 'p', 'P']); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const snapshotVersion = getOption(options, [ 'snapshot-version', 'snapshotVersion', diff --git a/src/lib/objects/put.ts b/src/lib/objects/put.ts index 0d00fe5..d4c7df1 100644 --- a/src/lib/objects/put.ts +++ b/src/lib/objects/put.ts @@ -27,7 +27,10 @@ export default async function putObject(options: Record) { 't', 'T', ]); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); if (!bucket) { printFailure(context, 'Bucket name is required'); diff --git a/src/lib/objects/set.ts b/src/lib/objects/set.ts index 84df854..d89926d 100644 --- a/src/lib/objects/set.ts +++ b/src/lib/objects/set.ts @@ -13,6 +13,11 @@ const context = msg('objects', 'set'); export default async function setObject(options: Record) { printStart(context); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const bucket = getOption(options, ['bucket']); const key = getOption(options, ['key']); const access = getOption(options, ['access', 'a', 'A']); @@ -49,5 +54,17 @@ export default async function setObject(options: Record) { process.exit(1); } + if (format === 'json') { + console.log( + JSON.stringify({ + action: 'updated', + bucket, + key, + access, + ...(newKey ? { newKey } : {}), + }) + ); + } + printSuccess(context, { key, bucket }); } diff --git a/src/lib/organizations/list.ts b/src/lib/organizations/list.ts index 5a08ff2..3c9a155 100644 --- a/src/lib/organizations/list.ts +++ b/src/lib/organizations/list.ts @@ -11,6 +11,7 @@ import { import { getAuthClient } from '../../auth/client.js'; import { isFlyUser, fetchOrganizationsFromUserInfo } from '../../auth/fly.js'; import Enquirer from 'enquirer'; +import { requireInteractive } from '../../utils/interactive.js'; import { printStart, printSuccess, @@ -42,7 +43,10 @@ export default async function list(options: Record) { return; } - const format = getOption(options, ['format', 'f', 'F'], 'select'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'select'); // For Fly users, fetch organizations from userinfo endpoint const authClient = getAuthClient(); @@ -86,6 +90,8 @@ export default async function list(options: Record) { hint: org.id === currentSelection ? 'currently selected' : undefined, })); + requireInteractive('Use --format table or --format json'); + const response = await Enquirer.prompt<{ organization: string }>({ type: 'select', name: 'organization', diff --git a/src/lib/presign.ts b/src/lib/presign.ts index 9045f04..4af628b 100644 --- a/src/lib/presign.ts +++ b/src/lib/presign.ts @@ -36,7 +36,10 @@ export default async function presign(options: Record) { getOption(options, ['expires-in', 'expiresIn', 'e']) ?? '3600', 10 ); - const format = getOption(options, ['format', 'f']) ?? 'url'; + const json = getOption(options, ['json']); + const format = json + ? 'json' + : (getOption(options, ['format', 'f']) ?? 'url'); const accessKeyFlag = getOption(options, ['access-key', 'accessKey']); const config = await getStorageConfig(); diff --git a/src/lib/rm.ts b/src/lib/rm.ts index 90c0017..937af3c 100644 --- a/src/lib/rm.ts +++ b/src/lib/rm.ts @@ -1,4 +1,3 @@ -import * as readline from 'readline'; import { isRemotePath, parseRemotePath, @@ -10,25 +9,19 @@ import { import { getOption } from '../utils/options.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { remove, removeBucket, list } from '@tigrisdata/storage'; +import { requireInteractive, confirm } from '../utils/interactive.js'; -async function confirm(message: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question(`${message} (y/N): `, (answer) => { - rl.close(); - resolve(answer.toLowerCase() === 'y'); - }); - }); -} +let _jsonMode = false; export default async function rm(options: Record) { const pathString = getOption(options, ['path']); - const force = getOption(options, ['force', 'f', 'F']); + const force = getOption(options, ['force', 'f', 'F', 'yes', 'y']); const recursive = !!getOption(options, ['recursive', 'r']); + const jsonFlag = getOption(options, ['json']); + const format = jsonFlag + ? 'json' + : getOption(options, ['format'], 'table'); + _jsonMode = format === 'json'; if (!pathString) { console.error('path argument is required'); @@ -53,11 +46,12 @@ export default async function rm(options: Record) { const rawEndsWithSlash = pathString.endsWith('/'); if (!path && !rawEndsWithSlash) { if (!force) { + requireInteractive('Use --yes to skip confirmation'); const confirmed = await confirm( `Are you sure you want to delete bucket '${bucket}'?` ); if (!confirmed) { - console.log('Aborted'); + if (!_jsonMode) console.log('Aborted'); return; } } @@ -69,7 +63,11 @@ export default async function rm(options: Record) { process.exit(1); } - console.log(`Removed bucket '${bucket}'`); + if (_jsonMode) { + console.log(JSON.stringify({ action: 'removed', bucket })); + } else { + console.log(`Removed bucket '${bucket}'`); + } return; } @@ -146,16 +144,21 @@ export default async function rm(options: Record) { const totalItems = itemsToRemove.length + (hasSeparateFolderMarker ? 1 : 0); if (totalItems === 0) { - console.log('No objects to remove'); + if (_jsonMode) { + console.log(JSON.stringify({ action: 'removed', count: 0 })); + } else { + console.log('No objects to remove'); + } return; } if (!force) { + requireInteractive('Use --yes to skip confirmation'); const confirmed = await confirm( `Are you sure you want to delete ${totalItems} object(s)?` ); if (!confirmed) { - console.log('Aborted'); + if (!_jsonMode) console.log('Aborted'); return; } } @@ -174,7 +177,7 @@ export default async function rm(options: Record) { if (removeError) { console.error(`Failed to remove ${item.name}: ${removeError.message}`); } else { - console.log(`Removed t3://${bucket}/${item.name}`); + if (!_jsonMode) console.log(`Removed t3://${bucket}/${item.name}`); removed++; } } @@ -193,20 +196,25 @@ export default async function rm(options: Record) { `Failed to remove ${folderMarker}: ${removeError.message}` ); } else { - console.log(`Removed t3://${bucket}/${folderMarker}`); + if (!_jsonMode) console.log(`Removed t3://${bucket}/${folderMarker}`); removed++; } } - console.log(`Removed ${removed} object(s)`); + if (_jsonMode) { + console.log(JSON.stringify({ action: 'removed', count: removed })); + } else { + console.log(`Removed ${removed} object(s)`); + } } else { // Remove single object if (!force) { + requireInteractive('Use --yes to skip confirmation'); const confirmed = await confirm( `Are you sure you want to delete 't3://${bucket}/${path}'?` ); if (!confirmed) { - console.log('Aborted'); + if (!_jsonMode) console.log('Aborted'); return; } } @@ -223,7 +231,17 @@ export default async function rm(options: Record) { process.exit(1); } - console.log(`Removed t3://${bucket}/${path}`); + if (_jsonMode) { + console.log( + JSON.stringify({ + action: 'removed', + count: 1, + path: `t3://${bucket}/${path}`, + }) + ); + } else { + console.log(`Removed t3://${bucket}/${path}`); + } } process.exit(0); } diff --git a/src/lib/snapshots/list.ts b/src/lib/snapshots/list.ts index e8a1785..0da6c44 100644 --- a/src/lib/snapshots/list.ts +++ b/src/lib/snapshots/list.ts @@ -16,7 +16,10 @@ export default async function list(options: Record) { printStart(context); const name = getOption(options, ['name']); - const format = getOption(options, ['format', 'f', 'F'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); if (!name) { printFailure(context, 'Bucket name is required'); diff --git a/src/lib/stat.ts b/src/lib/stat.ts index 76abb67..b201e7e 100644 --- a/src/lib/stat.ts +++ b/src/lib/stat.ts @@ -17,7 +17,10 @@ export default async function stat(options: Record) { printStart(context); const pathString = getOption(options, ['path']); - const format = getOption(options, ['format'], 'table'); + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const snapshotVersion = getOption(options, [ 'snapshot-version', 'snapshotVersion', diff --git a/src/lib/touch.ts b/src/lib/touch.ts index 5408c7c..0c98914 100644 --- a/src/lib/touch.ts +++ b/src/lib/touch.ts @@ -23,6 +23,11 @@ export default async function touch(options: Record) { process.exit(1); } + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); + const config = await getStorageConfig(); const { error } = await put(path, '', { @@ -37,6 +42,10 @@ export default async function touch(options: Record) { process.exit(1); } - console.log(`Created '${bucket}/${path}'`); + if (format === 'json') { + console.log(JSON.stringify({ action: 'created', bucket, path })); + } else { + console.log(`Created '${bucket}/${path}'`); + } process.exit(0); } diff --git a/src/lib/whoami.ts b/src/lib/whoami.ts index 3c00b9c..8929889 100644 --- a/src/lib/whoami.ts +++ b/src/lib/whoami.ts @@ -7,11 +7,18 @@ import { } from '../auth/storage.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { printFailure, printAlreadyDone, msg } from '../utils/messages.js'; +import { getOption } from '../utils/options.js'; const context = msg('whoami'); -export default async function whoami(): Promise { +export default async function whoami( + options: Record = {} +): Promise { try { + const json = getOption(options, ['json']); + const format = json + ? 'json' + : getOption(options, ['format', 'f', 'F'], 'table'); const loginMethod = getLoginMethod(); const credentials = getCredentials(); @@ -47,9 +54,12 @@ export default async function whoami(): Promise { lines.push(` User ID: ${userId || 'N/A'}`); // Only fetch organizations for OAuth users (credentials don't have session tokens) + let organizations: { id: string; name: string }[] = []; + let selectedOrg: string | null | undefined; + if (loginMethod === 'oauth') { const config = await getStorageConfig(); - const selectedOrg = getSelectedOrganization(); + selectedOrg = getSelectedOrganization(); const { data, error } = await listOrganizations({ config }); if (error) { @@ -57,7 +67,7 @@ export default async function whoami(): Promise { process.exit(1); } - const organizations = data?.organizations ?? []; + organizations = data?.organizations ?? []; if (organizations.length > 0) { lines.push(''); @@ -87,6 +97,22 @@ export default async function whoami(): Promise { ); } + if (format === 'json') { + const result: Record = { email, userId, loginMethod }; + if (loginMethod === 'oauth') { + result.organizations = organizations.map((org) => ({ + id: org.id, + name: org.name, + })); + if (selectedOrg) { + const selected = organizations.find((o) => o.id === selectedOrg); + if (selected) result.activeOrganization = selected.name; + } + } + console.log(JSON.stringify(result)); + return; + } + lines.push(''); console.log(lines.join('\n')); } catch (error) { diff --git a/src/specs.yaml b/src/specs.yaml index cedd2c4..5de5628 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -189,6 +189,15 @@ commands: onSuccess: '' onFailure: 'Failed to get user information' onAlreadyDone: "Not authenticated\nRun \"tigris login\" to authenticate" + arguments: + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # logout - name: logout @@ -223,6 +232,14 @@ commands: description: Bucket name to test access against (optional) alias: b required: false + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag ######################### @@ -252,6 +269,14 @@ 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: format + description: Output format + alias: f + options: [json, table, xml] + default: table + - name: json + description: Output as JSON + type: flag # mk - name: mk @@ -315,6 +340,14 @@ commands: - name: source-snapshot description: 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 alias: source-snap + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # touch - name: touch @@ -332,6 +365,14 @@ commands: examples: - my-bucket/my-file.txt - t3://my-bucket/my-file.txt + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # stat - name: stat @@ -358,6 +399,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - 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 @@ -398,6 +442,9 @@ commands: alias: f options: [url, json] default: url + - name: json + description: Output as JSON + type: flag # cp - name: cp @@ -434,6 +481,14 @@ commands: type: flag alias: r description: Copy directories recursively + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # mv - name: mv @@ -469,6 +524,13 @@ commands: type: flag alias: f description: Skip confirmation prompt + - name: format + description: Output format + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # rm - name: rm @@ -499,6 +561,13 @@ commands: type: flag alias: f description: Skip confirmation prompt + - name: format + description: Output format + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag ######################### # Manage organizations @@ -529,6 +598,9 @@ commands: alias: f options: [json, table, xml, select] default: select + - name: json + description: Output as JSON + type: flag - name: select description: Interactive selection mode alias: i @@ -598,6 +670,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - name: forks-of description: Only list buckets that are forks of the named source bucket # create @@ -660,6 +735,14 @@ commands: - name: source-snapshot description: 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 alias: source-snap + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # get - name: get description: Show details for a bucket including access level, region, tier, and custom domain @@ -677,13 +760,21 @@ commands: required: true examples: - my-bucket + - name: format + description: Output format + alias: f + options: [json, table, xml] + default: table + - name: json + description: Output as JSON + type: flag # delete - name: delete description: Delete one or more buckets by name. The bucket must be empty or delete-protection must be off alias: d examples: - - "tigris buckets delete my-bucket" - - "tigris buckets delete bucket-a,bucket-b" + - "tigris buckets delete my-bucket --force" + - "tigris buckets delete bucket-a,bucket-b --force" messages: onStart: 'Deleting bucket...' onSuccess: "Bucket '{{name}}' deleted successfully" @@ -696,6 +787,17 @@ commands: multiple: true examples: - my-bucket + - name: force + type: flag + description: Skip confirmation prompt + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # set - name: set description: Update settings on an existing bucket such as access level, location, caching, or custom domain @@ -743,6 +845,13 @@ commands: - name: enable-additional-headers description: Enable additional HTTP headers (X-Content-Type-Options nosniff) type: boolean + - name: format + description: Output format + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # 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 @@ -986,6 +1095,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag # create - name: create description: (Deprecated, use "buckets create --fork-of") Create a new fork (copy-on-write clone) of the source bucket @@ -1050,6 +1162,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag # take - name: take description: Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot @@ -1115,6 +1230,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - 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 @@ -1152,6 +1270,14 @@ 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: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # put - name: put description: Upload a local file as an object. Content-type is auto-detected from extension unless overridden @@ -1194,13 +1320,16 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag # delete - name: delete description: Delete one or more objects by key from the given bucket alias: d examples: - - "tigris objects delete my-bucket old-file.txt" - - "tigris objects delete my-bucket file-a.txt,file-b.txt" + - "tigris objects delete my-bucket old-file.txt --force" + - "tigris objects delete my-bucket file-a.txt,file-b.txt --force" messages: onStart: 'Deleting object...' onSuccess: "Object '{{key}}' deleted successfully" @@ -1219,6 +1348,17 @@ commands: multiple: true examples: - my-file.txt + - name: force + type: flag + description: Skip confirmation prompt + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag # set - name: set description: Update settings on an existing object such as access level @@ -1247,6 +1387,14 @@ commands: - name: new-key description: Rename the object to a new key alias: n + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag ######################### # Manage access keys @@ -1269,6 +1417,15 @@ commands: onSuccess: '' onFailure: 'Failed to list access keys' onEmpty: 'No access keys found' + arguments: + - name: format + description: Output format + alias: f + options: [json, table, xml] + default: table + - name: json + description: Output as JSON + type: flag - name: create description: Create a new access key with the given name. Returns the key ID and secret (shown only once) alias: c @@ -1285,11 +1442,19 @@ commands: required: true examples: - my-key + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag - name: delete description: Permanently delete an access key by its ID. This revokes all access immediately alias: d examples: - - "tigris access-keys delete tid_AaBbCcDdEeFf" + - "tigris access-keys delete tid_AaBbCcDdEeFf --force" messages: onStart: 'Deleting access key...' onSuccess: 'Access key deleted' @@ -1301,6 +1466,17 @@ commands: required: true examples: - tid_AaBbCcDdEeFf + - name: force + type: flag + description: Skip confirmation prompt + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag - name: get description: Show details for an access key including its name, creation date, and assigned bucket roles alias: g @@ -1317,6 +1493,14 @@ commands: required: true examples: - tid_AaBbCcDdEeFf + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag - name: assign description: Assign per-bucket roles to an access key. Pair each --bucket with a --role (Editor or ReadOnly), or use --admin for org-wide access alias: a @@ -1353,6 +1537,14 @@ commands: - name: revoke-roles description: Revoke all bucket roles from the access key type: flag + - name: format + description: Output format + alias: f + options: [json, table] + default: table + - name: json + description: Output as JSON + type: flag ######################### # IAM - Identity and Access Management @@ -1388,6 +1580,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - name: get description: Show details for a policy including its document and attached users. If no ARN provided, shows interactive selection alias: g @@ -1411,6 +1606,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - name: create description: Create a new policy with the given name and policy document. Document can be provided via file, inline JSON, or stdin alias: c @@ -1466,7 +1664,7 @@ commands: alias: d examples: - "tigris iam policies delete" - - "tigris iam policies delete arn:aws:iam::org_id:policy/my-policy" + - "tigris iam policies delete arn:aws:iam::org_id:policy/my-policy --force" messages: onStart: 'Deleting policy...' onSuccess: "Policy '{{resource}}' deleted" @@ -1479,6 +1677,9 @@ commands: required: false examples: - arn:aws:iam::org_id:policy/my-policy + - name: force + type: flag + description: Skip confirmation prompt - name: users description: Manage organization users and invitations @@ -1505,6 +1706,9 @@ commands: alias: f options: [json, table, xml] default: table + - name: json + description: Output as JSON + type: flag - name: invite description: Invite users to the organization by email alias: i @@ -1535,8 +1739,8 @@ commands: alias: ri examples: - "tigris iam users revoke-invitation" - - "tigris iam users revoke-invitation invitation_id" - - "tigris iam users revoke-invitation id1,id2,id3" + - "tigris iam users revoke-invitation invitation_id --force" + - "tigris iam users revoke-invitation id1,id2,id3 --force" messages: onStart: 'Revoking invitation...' onSuccess: "Invitation(s) revoked" @@ -1548,6 +1752,9 @@ commands: type: positional required: false multiple: true + - name: force + type: flag + description: Skip confirmation prompt - name: update-role description: Update user roles in the organization. If no user ID provided, shows interactive selection alias: ur @@ -1578,8 +1785,8 @@ commands: alias: rm examples: - "tigris iam users remove" - - "tigris iam users remove user@example.com" - - "tigris iam users remove user@example.com,user@example.net" + - "tigris iam users remove user@example.com --force" + - "tigris iam users remove user@example.com,user@example.net --force" messages: onStart: 'Removing user...' onSuccess: "User(s) removed" @@ -1591,3 +1798,6 @@ commands: type: positional required: false multiple: true + - name: force + type: flag + description: Skip confirmation prompt diff --git a/src/utils/interactive.ts b/src/utils/interactive.ts new file mode 100644 index 0000000..a528214 --- /dev/null +++ b/src/utils/interactive.ts @@ -0,0 +1,33 @@ +import * as readline from 'readline'; + +/** + * Guard for interactive-only operations. + * Fails fast with a helpful hint when stdin is not a TTY + * (e.g., when called from a script or AI agent). + */ +export function requireInteractive(hint: string): void { + if (process.stdin.isTTY) return; + console.error( + 'Error: this command requires interactive input (not available in piped/scripted mode)' + ); + console.error(`Hint: ${hint}`); + process.exit(1); +} + +/** + * Prompt the user for y/N confirmation via readline. + * Returns true only if the user types "y" (case-insensitive). + */ +export async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${message} (y/N): `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y'); + }); + }); +} diff --git a/src/utils/locations.ts b/src/utils/locations.ts index 7e27fab..5e502ad 100644 --- a/src/utils/locations.ts +++ b/src/utils/locations.ts @@ -1,5 +1,6 @@ import type { BucketLocations } from '@tigrisdata/storage'; import enquirer from 'enquirer'; +import { requireInteractive } from './interactive.js'; const { prompt } = enquirer; @@ -86,6 +87,8 @@ async function promptRegion( } export async function promptLocations(): Promise { + requireInteractive('Provide --locations flag'); + let locationType: string; try { ({ locationType } = await prompt<{ locationType: string }>({ diff --git a/test/cli-core.test.ts b/test/cli-core.test.ts new file mode 100644 index 0000000..6478107 --- /dev/null +++ b/test/cli-core.test.ts @@ -0,0 +1,357 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + isValidCommandName, + formatArgumentHelp, + extractArgumentValues, + validateRequiredWhen, + addArgumentsToCommand, +} from '../src/cli-core.js'; +import type { Command as CommanderCommand } from 'commander'; +import type { Argument } from '../src/types.js'; + +describe('isValidCommandName', () => { + it.each(['buckets', 'access-keys', 'set_ttl', 'ls', 'a1'])( + 'accepts valid name: %s', + (name) => { + expect(isValidCommandName(name)).toBe(true); + } + ); + + it.each(['', '../etc', 'foo bar', 'rm;ls', 'a/b', 'cmd@1'])( + 'rejects invalid name: %s', + (name) => { + expect(isValidCommandName(name)).toBe(false); + } + ); +}); + +describe('formatArgumentHelp', () => { + it('formats positional argument', () => { + const arg: Argument = { + name: 'path', + description: 'The file path', + type: 'positional', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain(' path'); + expect(result).toContain('[positional argument]'); + }); + + it('formats flag', () => { + const arg: Argument = { + name: 'force', + description: 'Force the operation', + type: 'flag', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('--force'); + }); + + it('formats short alias', () => { + const arg: Argument = { + name: 'format', + description: 'Output format', + alias: 'f', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('--format, -f'); + }); + + it('formats long alias', () => { + const arg: Argument = { + name: 'fork-of', + description: 'Fork source', + alias: 'fork', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('--fork-of, --fork'); + }); + + it('formats string[] options', () => { + const arg: Argument = { + name: 'format', + description: 'Output format', + options: ['json', 'table'], + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('(options: json, table)'); + }); + + it('formats object options', () => { + const arg: Argument = { + name: 'tier', + description: 'Storage tier', + options: [ + { name: 'Standard', value: 'STANDARD', description: 'Default tier' }, + ], + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('(options: STANDARD)'); + }); + + it('formats default value', () => { + const arg: Argument = { + name: 'format', + description: 'Output format', + default: 'table', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('[default: table]'); + }); + + it('formats required', () => { + const arg: Argument = { + name: 'name', + description: 'Bucket name', + required: true, + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('[required]'); + }); + + it('formats required-when', () => { + const arg: Argument = { + name: 'target', + description: 'Target bucket', + 'required-when': 'type=bucket', + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('[required when: type=bucket]'); + }); + + it('formats multiple', () => { + const arg: Argument = { + name: 'regions', + description: 'Regions', + multiple: true, + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('[multiple values: comma-separated]'); + }); + + it('formats examples', () => { + const arg: Argument = { + name: 'path', + description: 'Object path', + examples: ['t3://bucket/key'], + }; + const result = formatArgumentHelp(arg); + expect(result).toContain('(examples: t3://bucket/key)'); + }); + + it('pads short names to at least 26 chars', () => { + const arg: Argument = { name: 'x', description: 'desc' }; + const result = formatArgumentHelp(arg); + // " --x" is 5 chars, should be padded to 26 + const descIndex = result.indexOf('desc'); + expect(descIndex).toBeGreaterThanOrEqual(26); + }); +}); + +describe('extractArgumentValues', () => { + it('passes through plain object', () => { + const args: Argument[] = []; + const result = extractArgumentValues(args, [], { foo: 'bar' }); + expect(result).toEqual({ foo: 'bar' }); + }); + + it('calls optsWithGlobals() when present', () => { + const args: Argument[] = []; + const commandObj = { + optsWithGlobals: () => ({ fromGlobals: true }), + }; + const result = extractArgumentValues( + args, + [], + commandObj as unknown as Record + ); + expect(result).toEqual({ fromGlobals: true }); + }); + + it('calls opts() when present (no optsWithGlobals)', () => { + const args: Argument[] = []; + const commandObj = { + opts: () => ({ fromOpts: true }), + }; + const result = extractArgumentValues( + args, + [], + commandObj as unknown as Record + ); + expect(result).toEqual({ fromOpts: true }); + }); + + it('maps positional args by index', () => { + const args: Argument[] = [ + { name: 'source', description: 'Source', type: 'positional' }, + { name: 'dest', description: 'Destination', type: 'positional' }, + ]; + const result = extractArgumentValues(args, ['a.txt', 'b.txt'], {}); + expect(result.source).toBe('a.txt'); + expect(result.dest).toBe('b.txt'); + }); + + it('comma-splits multiple positional args', () => { + const args: Argument[] = [ + { + name: 'regions', + description: 'Regions', + type: 'positional', + multiple: true, + }, + ]; + const result = extractArgumentValues(args, ['ams,fra,sjc'], {}); + expect(result.regions).toEqual(['ams', 'fra', 'sjc']); + }); + + it('comma-splits multiple non-positional string values', () => { + const args: Argument[] = [ + { name: 'tags', description: 'Tags', multiple: true }, + ]; + const result = extractArgumentValues(args, [], { tags: 'a,b,c' }); + expect(result.tags).toEqual(['a', 'b', 'c']); + }); +}); + +describe('validateRequiredWhen', () => { + let errorSpy: ReturnType; + + beforeEach(() => { + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + }); + + it('returns true when no required args', () => { + const args: Argument[] = [ + { name: 'format', description: 'Format' }, + ]; + expect(validateRequiredWhen(args, {})).toBe(true); + }); + + it('returns true when required arg is present', () => { + const args: Argument[] = [ + { name: 'name', description: 'Name', required: true }, + ]; + expect(validateRequiredWhen(args, { name: 'test' })).toBe(true); + }); + + it('returns false when required arg is missing', () => { + const args: Argument[] = [ + { name: 'name', description: 'Name', required: true }, + ]; + expect(validateRequiredWhen(args, {})).toBe(false); + expect(errorSpy).toHaveBeenCalledWith('--name is required'); + }); + + it('returns false when required-when condition met but value missing', () => { + const args: Argument[] = [ + { name: 'type', description: 'Type' }, + { + name: 'target', + description: 'Target', + 'required-when': 'type=bucket', + }, + ]; + expect(validateRequiredWhen(args, { type: 'bucket' })).toBe(false); + expect(errorSpy).toHaveBeenCalledWith( + '--target is required when --type is bucket' + ); + }); + + it('returns true when required-when condition not met', () => { + const args: Argument[] = [ + { name: 'type', description: 'Type' }, + { + name: 'target', + description: 'Target', + 'required-when': 'type=bucket', + }, + ]; + expect(validateRequiredWhen(args, { type: 'object' })).toBe(true); + }); + + it('returns true when required-when condition met and value present', () => { + const args: Argument[] = [ + { name: 'type', description: 'Type' }, + { + name: 'target', + description: 'Target', + 'required-when': 'type=bucket', + }, + ]; + expect( + validateRequiredWhen(args, { type: 'bucket', target: 'my-bucket' }) + ).toBe(true); + }); +}); + +describe('addArgumentsToCommand', () => { + function createMockCmd() { + const calls = { + argument: [] as Array<[string, string]>, + option: [] as Array<[string, string, string?]>, + }; + const cmd = { + argument(name: string, desc: string) { + calls.argument.push([name, desc]); + return cmd; + }, + option(flags: string, desc: string, defaultVal?: string) { + calls.option.push([flags, desc, defaultVal]); + return cmd; + }, + }; + return { cmd, calls }; + } + + it('adds required positional as ', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'path', description: 'Path', type: 'positional', required: true }, + ]); + expect(calls.argument[0][0]).toBe(''); + }); + + it('adds optional positional as [name]', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'path', description: 'Path', type: 'positional' }, + ]); + expect(calls.argument[0][0]).toBe('[path]'); + }); + + it('adds flag without value placeholder', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'force', description: 'Force', type: 'flag' }, + ]); + expect(calls.option[0][0]).toBe('--force'); + }); + + it('adds short alias', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'format', description: 'Format', alias: 'f', options: ['json', 'table'] }, + ]); + expect(calls.option[0][0]).toBe('-f, --format '); + }); + + it('adds long alias', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'fork-of', description: 'Fork', alias: 'fork', required: true }, + ]); + expect(calls.option[0][0]).toBe('--fork, --fork-of '); + }); + + it('passes default value as 3rd arg', () => { + const { cmd, calls } = createMockCmd(); + addArgumentsToCommand(cmd as unknown as CommanderCommand, [ + { name: 'format', description: 'Format', default: 'table' }, + ]); + expect(calls.option[0][2]).toBe('table'); + }); +}); diff --git a/test/cli.test.ts b/test/cli.test.ts index ae32053..2ba5a46 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,14 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { execSync } from 'child_process'; +import { + existsSync, + readFileSync, + writeFileSync, + mkdirSync, + rmSync, +} from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; import { shouldSkipIntegrationTests, getTestPrefix } from './setup.js'; const skipTests = shouldSkipIntegrationTests(); @@ -14,7 +23,7 @@ function runCli(args: string): { const stdout = execSync(`node dist/cli.js ${args}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin to prevent hanging on prompts - timeout: 30000, // 30 second timeout per command + timeout: 60000, // 60 second timeout per command env: { ...process.env, // Pass through auth env vars @@ -232,12 +241,33 @@ describe('CLI Help Commands', () => { }); }); +describe('Destructive commands require --force in non-TTY', () => { + // These tests verify that destructive commands refuse to run without --force + // when stdin is not a TTY (piped/scripted mode). Since runCli uses + // stdio: ['ignore', ...], stdin is not a TTY. + + it('objects delete should require confirmation in non-TTY', () => { + const result = runCli('objects delete fake-bucket fake-key'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Use --yes to skip confirmation'); + }); + + it('buckets delete should require confirmation in non-TTY', () => { + const result = runCli('buckets delete fake-bucket'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Use --yes to skip confirmation'); + }); +}); + describe.skipIf(skipTests)('CLI Integration Tests', () => { // Generate unique prefix for all test resources const testPrefix = getTestPrefix(); const testBucket = testPrefix; const testContent = 'Hello from CLI test'; + /** Prefix a bucket/path with t3:// for commands that require remote paths (cp, mv, rm) */ + const t3 = (path: string) => `t3://${path}`; + beforeAll(async () => { // Setup credentials from .env console.log('Setting up credentials from .env...'); @@ -248,7 +278,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { console.log(`Test prefix: ${testPrefix}`); console.log(`Creating test bucket: ${testBucket}`); // Use mk command instead of buckets create to avoid interactive prompts - const result = runCli(`mk ${testBucket}`); + const result = runCli(`mk ${testBucket} --enable-snapshots`); if (result.exitCode !== 0) { console.error('Failed to create test bucket:', result.stderr); throw new Error('Failed to create test bucket'); @@ -258,8 +288,8 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { afterAll(async () => { console.log(`Cleaning up test bucket: ${testBucket}`); // Force remove all objects and the bucket - runCli(`rm ${testBucket}/* -f`); - runCli(`rm ${testBucket} -f`); + runCli(`rm ${t3(testBucket)}/* -f`); + runCli(`rm ${t3(testBucket)} -f`); }); describe('ls command', () => { @@ -347,7 +377,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should copy an object within same bucket', () => { const result = runCli( - `cp ${testBucket}/${srcFile} ${testBucket}/${destFile}` + `cp ${t3(testBucket)}/${srcFile} ${t3(testBucket)}/${destFile}` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -370,7 +400,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should move an object with force flag', () => { const result = runCli( - `mv ${testBucket}/${srcFile} ${testBucket}/${destFile} -f` + `mv ${t3(testBucket)}/${srcFile} ${t3(testBucket)}/${destFile} -f` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Moved'); @@ -392,7 +422,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); it('should remove an object with force flag', () => { - const result = runCli(`rm ${testBucket}/${fileName} -f`); + const result = runCli(`rm ${t3(testBucket)}/${fileName} -f`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Removed'); }); @@ -418,7 +448,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should auto-detect folder for cp without trailing slash', () => { const result = runCli( - `cp ${testBucket}/${autoFolder} ${testBucket}/${copiedFolder}` + `cp ${t3(testBucket)}/${autoFolder} ${t3(testBucket)}/${copiedFolder} -r` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -427,21 +457,22 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should auto-detect folder for mv without trailing slash', () => { const result = runCli( - `mv ${testBucket}/${copiedFolder} ${testBucket}/${movedFolder} -f` + `mv ${t3(testBucket)}/${copiedFolder} ${t3(testBucket)}/${movedFolder} -r -f` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Moved'); - expect(result.stdout).toContain('2 object(s)'); + // cp nests: autodetect → copied/autodetect/ (marker + 2 files = 3) + expect(result.stdout).toContain('3 object(s)'); }); it('should auto-detect folder for rm without trailing slash', () => { - const result = runCli(`rm ${testBucket}/${movedFolder} -f`); + const result = runCli(`rm ${t3(testBucket)}/${movedFolder} -r -f`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Removed'); }); afterAll(() => { - runCli(`rm ${testBucket}/${autoFolder} -f`); + runCli(`rm ${t3(testBucket)}/${autoFolder} -r -f`); }); }); @@ -457,7 +488,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should copy an empty folder', () => { const result = runCli( - `cp ${testBucket}/${emptyFolder}/ ${testBucket}/${copiedEmptyFolder}/` + `cp ${t3(testBucket)}/${emptyFolder}/ ${t3(testBucket)}/${copiedEmptyFolder}/ -r` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -472,7 +503,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should move an empty folder', () => { const result = runCli( - `mv ${testBucket}/${copiedEmptyFolder}/ ${testBucket}/${movedEmptyFolder}/ -f` + `mv ${t3(testBucket)}/${copiedEmptyFolder}/ ${t3(testBucket)}/${movedEmptyFolder}/ -r -f` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Moved'); @@ -487,8 +518,8 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); afterAll(() => { - runCli(`rm ${testBucket}/${emptyFolder}/ -f`); - runCli(`rm ${testBucket}/${movedEmptyFolder}/ -f`); + runCli(`rm ${t3(testBucket)}/${emptyFolder}/ -r -f`); + runCli(`rm ${t3(testBucket)}/${movedEmptyFolder}/ -r -f`); }); }); @@ -508,7 +539,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should copy file to existing folder (auto-detect)', () => { const result = runCli( - `cp ${testBucket}/${srcFile} ${testBucket}/${targetFolder}` + `cp ${t3(testBucket)}/${srcFile} ${t3(testBucket)}/${targetFolder}` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -517,7 +548,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should copy file to explicit folder path (trailing slash)', () => { const result = runCli( - `cp ${testBucket}/${srcFile2} ${testBucket}/${targetFolder}/` + `cp ${t3(testBucket)}/${srcFile2} ${t3(testBucket)}/${targetFolder}/` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -526,7 +557,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should move file to existing folder with force flag', () => { const result = runCli( - `mv ${testBucket}/${srcFile3} ${testBucket}/${targetFolder} -f` + `mv ${t3(testBucket)}/${srcFile3} ${t3(testBucket)}/${targetFolder} -f` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Moved'); @@ -542,9 +573,9 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); afterAll(() => { - runCli(`rm ${testBucket}/${targetFolder}/ -f`); - runCli(`rm ${testBucket}/${srcFile} -f`); - runCli(`rm ${testBucket}/${srcFile2} -f`); + runCli(`rm ${t3(testBucket)}/${targetFolder}/ -r -f`); + runCli(`rm ${t3(testBucket)}/${srcFile} -f`); + runCli(`rm ${t3(testBucket)}/${srcFile2} -f`); }); }); @@ -568,13 +599,13 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); it('should error on cp with bucket-only source', () => { - const result = runCli(`cp ${testBucket} ${testBucket}-other/`); + const result = runCli(`cp ${t3(testBucket)} ${t3(testBucket)}-other/`); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('Cannot copy a bucket'); }); it('should error on mv with bucket-only source', () => { - const result = runCli(`mv ${testBucket} ${testBucket}-other/`); + const result = runCli(`mv ${t3(testBucket)} ${t3(testBucket)}-other/`); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('Cannot move a bucket'); }); @@ -590,7 +621,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); it('should remove files matching wildcard pattern', () => { - const result = runCli(`rm ${testBucket}/${wildcardPrefix}-* -f`); + const result = runCli(`rm ${t3(testBucket)}/${wildcardPrefix}-* -f`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Removed'); expect(result.stdout).toContain('3 object(s)'); @@ -617,7 +648,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should copy folder contents and marker using wildcard', () => { const result = runCli( - `cp ${testBucket}/${wcFolder}/* ${testBucket}/${wcCopied}/` + `cp ${t3(testBucket)}/${wcFolder}/* ${t3(testBucket)}/${wcCopied}/` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Copied'); @@ -632,7 +663,7 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { it('should move folder contents and marker using wildcard', () => { const result = runCli( - `mv ${testBucket}/${wcCopied}/* ${testBucket}/${wcMoved}/ -f` + `mv ${t3(testBucket)}/${wcCopied}/* ${t3(testBucket)}/${wcMoved}/ -f` ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Moved'); @@ -647,8 +678,1155 @@ describe.skipIf(skipTests)('CLI Integration Tests', () => { }); afterAll(() => { - runCli(`rm ${testBucket}/${wcFolder}/ -f`); - runCli(`rm ${testBucket}/${wcMoved}/ -f`); + runCli(`rm ${t3(testBucket)}/${wcFolder}/ -r -f`); + runCli(`rm ${t3(testBucket)}/${wcMoved}/ -r -f`); + }); + }); + + // ─── Section A: Missing branches in already-tested commands ─── + + describe('mk command - bucket creation variants', () => { + const mkBuckets: string[] = []; + + afterAll(() => { + for (const b of mkBuckets) { + runCli(`rm ${t3(b)} -f`); + } + }); + + it('should create a public bucket with --public', () => { + const name = `${testPrefix}-mk-pub`; + mkBuckets.push(name); + const result = runCli(`mk ${name} --public`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + }); + + it('should create a bucket with --enable-snapshots', () => { + const name = `${testPrefix}-mk-snap`; + mkBuckets.push(name); + const result = runCli(`mk ${name} --enable-snapshots`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + + // Verify snapshots enabled via buckets get (table format) + const info = runCli(`buckets get ${name}`); + expect(info.exitCode).toBe(0); + expect(info.stdout).toContain('Snapshots Enabled'); + expect(info.stdout).toContain('Yes'); + }); + + it('should create a bucket with --default-tier STANDARD_IA', () => { + const name = `${testPrefix}-mk-tier`; + mkBuckets.push(name); + const result = runCli(`mk ${name} --default-tier STANDARD_IA`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + }); + + it('should create a bucket with --locations usa', () => { + const name = `${testPrefix}-mk-loc`; + mkBuckets.push(name); + const result = runCli(`mk ${name} --locations usa`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + }); + + it('should create a bucket with --fork-of', () => { + const name = `${testPrefix}-mk-fork`; + mkBuckets.push(name); + const result = runCli(`mk ${name} --fork-of ${testBucket}`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + }); + + it('should error on --source-snapshot without --fork-of', () => { + const result = runCli( + `mk ${testPrefix}-mk-nofork --source-snapshot snap1` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('--source-snapshot requires --fork-of'); + }); + + it('should error on no path argument', () => { + const result = runCli('mk'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('required'); + }); + }); + + describe('touch command - validation', () => { + it('should error on bucket-only path', () => { + const result = runCli(`touch ${testBucket}`); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Object key is required'); + }); + }); + + describe('cp command - local/remote operations', () => { + const tmpBase = join(tmpdir(), `cli-test-cp-${testPrefix}`); + + beforeAll(() => { + mkdirSync(tmpBase, { recursive: true }); + // Create test content remotely for download tests + const tmpUp = join(tmpBase, 'upload-src.txt'); + writeFileSync(tmpUp, testContent); + runCli(`objects put ${testBucket} cp-dl-test.txt ${tmpUp}`); + }); + + afterAll(() => { + rmSync(tmpBase, { recursive: true, force: true }); + runCli(`rm ${t3(testBucket)}/cp-dl-test.txt -f`); + runCli(`rm ${t3(testBucket)}/cp-ul-test.txt -f`); + runCli(`rm ${t3(testBucket)}/cp-ul-dir/ -r -f`); + runCli(`rm ${t3(testBucket)}/cp-wc-dest/ -r -f`); + runCli(`rm ${t3(testBucket)}/cp-dl-dir/ -r -f`); + }); + + it('should download a remote file to local path', () => { + const localDest = join(tmpBase, 'downloaded.txt'); + const result = runCli( + `cp ${t3(testBucket)}/cp-dl-test.txt ${localDest}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Downloaded'); + expect(existsSync(localDest)).toBe(true); + }); + + it('should upload a local file to remote', () => { + const localSrc = join(tmpBase, 'to-upload.txt'); + writeFileSync(localSrc, 'upload test content'); + const result = runCli( + `cp ${localSrc} ${t3(testBucket)}/cp-ul-test.txt` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Uploaded'); + + // Verify it exists + const ls = runCli(`ls ${testBucket}`); + expect(ls.stdout).toContain('cp-ul-test.txt'); + }); + + it('should upload a local directory recursively with -r', () => { + const localDir = join(tmpBase, 'upload-dir'); + mkdirSync(localDir, { recursive: true }); + writeFileSync(join(localDir, 'a.txt'), 'file-a'); + writeFileSync(join(localDir, 'b.txt'), 'file-b'); + const result = runCli( + `cp ${localDir}/ ${t3(testBucket)}/cp-ul-dir/ -r` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Uploaded'); + expect(result.stdout).toContain('2 file(s)'); + }); + + it('should download a remote directory recursively with -r', () => { + // Create remote files + runCli(`touch ${testBucket}/cp-dl-dir/x.txt`); + runCli(`touch ${testBucket}/cp-dl-dir/y.txt`); + const localDest = join(tmpBase, 'dl-dir'); + mkdirSync(localDest, { recursive: true }); + const result = runCli( + `cp ${t3(testBucket)}/cp-dl-dir/ ${localDest} -r` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Downloaded'); + expect(result.stdout).toContain('2 file(s)'); + }); + + it('should copy objects matching wildcard pattern', () => { + runCli(`touch ${testBucket}/cp-wc-a.txt`); + runCli(`touch ${testBucket}/cp-wc-b.txt`); + const result = runCli( + `cp ${t3(testBucket)}/cp-wc-* ${t3(testBucket)}/cp-wc-dest/` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Copied'); + expect(result.stdout).toContain('2 object(s)'); + + // Cleanup source wildcard files + runCli(`rm ${t3(testBucket)}/cp-wc-a.txt -f`); + runCli(`rm ${t3(testBucket)}/cp-wc-b.txt -f`); + }); + }); + + describe('mv command - additional branches', () => { + it('should move objects matching wildcard with -f', () => { + runCli(`touch ${testBucket}/mv-wc-a.txt`); + runCli(`touch ${testBucket}/mv-wc-b.txt`); + const result = runCli( + `mv ${t3(testBucket)}/mv-wc-* ${t3(testBucket)}/mv-wc-dest/ -f` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Moved'); + expect(result.stdout).toContain('2 object(s)'); + + // Cleanup + runCli(`rm ${t3(testBucket)}/mv-wc-dest/ -r -f`); + }); + + it('should error on folder move without -r', () => { + runCli(`mk ${testBucket}/mv-no-r/`); + runCli(`touch ${testBucket}/mv-no-r/file.txt`); + const result = runCli( + `mv ${t3(testBucket)}/mv-no-r ${t3(testBucket)}/mv-no-r-dest -f` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Use -r to move recursively'); + + // Cleanup + runCli(`rm ${t3(testBucket)}/mv-no-r/ -r -f`); + }); + }); + + describe('rm command - additional branches', () => { + it('should delete a bucket with -f', () => { + const name = `${testPrefix}-rm-bkt`; + runCli(`mk ${name}`); + const result = runCli(`rm ${t3(name)} -f`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`Removed bucket '${name}'`); + }); + + it('should error on folder removal without -r', () => { + runCli(`mk ${testBucket}/rm-no-r/`); + runCli(`touch ${testBucket}/rm-no-r/file.txt`); + const result = runCli(`rm ${t3(testBucket)}/rm-no-r -f`); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Use -r to remove recursively'); + + // Cleanup + runCli(`rm ${t3(testBucket)}/rm-no-r/ -r -f`); + }); + }); + + describe('objects get - additional branches', () => { + const tmpBase = join(tmpdir(), `cli-test-objget-${testPrefix}`); + + beforeAll(() => { + mkdirSync(tmpBase, { recursive: true }); + // Upload a text file for get tests + const tmpFile = join(tmpBase, 'src.txt'); + writeFileSync(tmpFile, testContent); + runCli(`objects put ${testBucket} objget-test.txt ${tmpFile}`); + }); + + afterAll(() => { + rmSync(tmpBase, { recursive: true, force: true }); + runCli(`rm ${t3(testBucket)}/objget-test.txt -f`); + }); + + it('should get object with --output to file', () => { + const outPath = join(tmpBase, 'output.txt'); + const result = runCli( + `objects get ${testBucket} objget-test.txt --output ${outPath}` + ); + expect(result.exitCode).toBe(0); + expect(existsSync(outPath)).toBe(true); + const content = readFileSync(outPath, 'utf-8'); + expect(content).toContain(testContent); + }); + + it('should get object with --mode string', () => { + const result = runCli( + `objects get ${testBucket} objget-test.txt --mode string` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(testContent); + }); + }); + + describe('objects put - additional branches', () => { + const tmpBase = join(tmpdir(), `cli-test-objput-${testPrefix}`); + + beforeAll(() => { + mkdirSync(tmpBase, { recursive: true }); + }); + + afterAll(() => { + rmSync(tmpBase, { recursive: true, force: true }); + runCli(`rm ${t3(testBucket)}/objput-pub.txt -f`); + runCli(`rm ${t3(testBucket)}/objput-ct.json -f`); + runCli(`rm ${t3(testBucket)}/objput-fmt.txt -f`); + }); + + it('should upload with --access public', () => { + const tmpFile = join(tmpBase, 'pub.txt'); + writeFileSync(tmpFile, 'public content'); + const result = runCli( + `objects put ${testBucket} objput-pub.txt ${tmpFile} --access public` + ); + expect(result.exitCode).toBe(0); + }); + + it('should upload with --content-type application/json', () => { + const tmpFile = join(tmpBase, 'ct.json'); + writeFileSync(tmpFile, '{"key":"value"}'); + const result = runCli( + `objects put ${testBucket} objput-ct.json ${tmpFile} --content-type application/json` + ); + expect(result.exitCode).toBe(0); + }); + + it('should upload with --format json', () => { + const tmpFile = join(tmpBase, 'fmt.txt'); + writeFileSync(tmpFile, 'format test'); + const result = runCli( + `objects put ${testBucket} objput-fmt.txt ${tmpFile} --format json` + ); + expect(result.exitCode).toBe(0); + // stdout contains progress line then JSON; extract just the JSON portion + const jsonStart = result.stdout.indexOf('['); + expect(jsonStart).toBeGreaterThanOrEqual(0); + expect(() => JSON.parse(result.stdout.slice(jsonStart))).not.toThrow(); + }); + }); + + describe('objects list - additional branches', () => { + beforeAll(() => { + runCli(`touch ${testBucket}/objlist-a.txt`); + runCli(`touch ${testBucket}/objlist-b.txt`); + }); + + afterAll(() => { + runCli(`rm ${t3(testBucket)}/objlist-a.txt -f`); + runCli(`rm ${t3(testBucket)}/objlist-b.txt -f`); + }); + + it('should list with --prefix filter', () => { + const result = runCli( + `objects list ${testBucket} --prefix objlist-a` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('objlist-a.txt'); + expect(result.stdout).not.toContain('objlist-b.txt'); + }); + + it('should list with --format json', () => { + const result = runCli( + `objects list ${testBucket} --format json` + ); + expect(result.exitCode).toBe(0); + expect(() => JSON.parse(result.stdout.trim())).not.toThrow(); + }); + + it('should handle empty results gracefully', () => { + const result = runCli( + `objects list ${testBucket} --prefix nonexistent-prefix-xyz` + ); + expect(result.exitCode).toBe(0); + }); + }); + + // ─── Section B: Completely untested commands ─── + + describe('stat command', () => { + beforeAll(() => { + runCli(`touch ${testBucket}/stat-test.txt`); + }); + + afterAll(() => { + runCli(`rm ${t3(testBucket)}/stat-test.txt -f`); + }); + + it('should show overall stats (no path)', () => { + const result = runCli('stat'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Active Buckets'); + expect(result.stdout).toContain('Total Objects'); + }); + + it('should show bucket info', () => { + const result = runCli(`stat ${testBucket}`); + expect(result.exitCode).toBe(0); + // Bucket stat shows a table with metrics + expect(result.stdout).toContain('Metric'); + }); + + it('should show object metadata', () => { + const result = runCli(`stat ${testBucket}/stat-test.txt`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Size'); + expect(result.stdout).toContain('Content-Type'); + }); + + it('should output --format json for overall stats', () => { + const result = runCli('stat --format json'); + expect(result.exitCode).toBe(0); + expect(() => JSON.parse(result.stdout.trim())).not.toThrow(); + }); + + it('should output --format json for bucket info', () => { + const result = runCli(`stat ${testBucket} --format json`); + expect(result.exitCode).toBe(0); + expect(() => JSON.parse(result.stdout.trim())).not.toThrow(); + }); + + it('should output --format json for object info', () => { + const result = runCli( + `stat ${testBucket}/stat-test.txt --format json` + ); + expect(result.exitCode).toBe(0); + expect(() => JSON.parse(result.stdout.trim())).not.toThrow(); + }); + }); + + describe('presign command', () => { + const accessKey = process.env.TIGRIS_STORAGE_ACCESS_KEY_ID!; + + beforeAll(() => { + runCli(`touch ${testBucket}/presign-test.txt`); + }); + + afterAll(() => { + runCli(`rm ${t3(testBucket)}/presign-test.txt -f`); + }); + + it('should generate presigned GET URL', () => { + const result = runCli( + `presign ${testBucket}/presign-test.txt --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toMatch(/^https:\/\//); + }); + + it('should generate presigned PUT URL with --method put', () => { + const result = runCli( + `presign ${testBucket}/presign-test.txt --method put --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toMatch(/^https:\/\//); + }); + + it('should accept --expires-in 600', () => { + const result = runCli( + `presign ${testBucket}/presign-test.txt --expires-in 600 --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(0); + }); + + it('should output --format json', () => { + const result = runCli( + `presign ${testBucket}/presign-test.txt --format json --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed).toHaveProperty('url'); + expect(parsed).toHaveProperty('method'); + expect(parsed).toHaveProperty('bucket'); + expect(parsed).toHaveProperty('key'); + }); + + it('should output URL-only with default format', () => { + const result = runCli( + `presign ${testBucket}/presign-test.txt --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(0); + // Should not be JSON, just a URL + expect(() => JSON.parse(result.stdout.trim())).toThrow(); + expect(result.stdout.trim()).toMatch(/^https:\/\//); + }); + + it('should error without path', () => { + const result = runCli('presign'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('required'); + }); + + it('should error on bucket-only path', () => { + const result = runCli( + `presign ${testBucket} --access-key ${accessKey}` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Object key is required'); + }); + }); + + describe('buckets list command', () => { + it('should list buckets', () => { + const result = runCli('buckets list'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(testBucket); + }); + + it('should list buckets with --format json', () => { + const result = runCli('buckets list --format json'); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout.trim()); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.some((b: { name: string }) => b.name === testBucket)).toBe( + true + ); + }); + }); + + describe('buckets get command', () => { + it('should get bucket info', () => { + const result = runCli(`buckets get ${testBucket}`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Property'); + }); + + it('should error without bucket name', () => { + const result = runCli('buckets get'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("missing required argument 'name'"); + }); + }); + + describe('buckets delete command', () => { + it('should delete a single bucket with --force', () => { + const name = `${testPrefix}-bd-1`; + runCli(`mk ${name}`); + const result = runCli(`buckets delete ${name} --force`); + expect(result.exitCode).toBe(0); + }); + + it('should delete multiple buckets with --force', () => { + const name1 = `${testPrefix}-bd-2`; + const name2 = `${testPrefix}-bd-3`; + runCli(`mk ${name1}`); + runCli(`mk ${name2}`); + const result = runCli(`buckets delete ${name1},${name2} --force`); + expect(result.exitCode).toBe(0); + }); + + it('should fail without --force in non-TTY', () => { + const name = `${testPrefix}-bd-nf`; + runCli(`mk ${name}`); + const result = runCli(`buckets delete ${name}`); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('--yes'); + // Cleanup + runCli(`buckets delete ${name} --force`); + }); + }); + + describe('buckets create command (non-interactive)', () => { + const bcBuckets: string[] = []; + + afterAll(() => { + for (const b of bcBuckets) { + runCli(`rm ${t3(b)} -f`); + } + }); + + it('should create with positional name', () => { + const name = `${testPrefix}-bc-1`; + bcBuckets.push(name); + const result = runCli(`buckets create ${name}`); + expect(result.exitCode).toBe(0); + }); + + it('should create with all flags', () => { + const name = `${testPrefix}-bc-all`; + bcBuckets.push(name); + const result = runCli( + `buckets create ${name} --access private --default-tier STANDARD --enable-snapshots --locations global` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error on --source-snapshot without --fork-of', () => { + const result = runCli( + `buckets create ${testPrefix}-bc-err --source-snapshot snap1` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('--source-snapshot requires --fork-of'); + }); + }); + + describe('bucket settings commands', () => { + const setBucket = `${testPrefix}-set`; + + beforeAll(() => { + runCli(`mk ${setBucket}`); + }); + + afterAll(() => { + // Disable delete protection before cleanup + runCli( + `buckets set ${setBucket} --enable-delete-protection false` + ); + runCli(`rm ${t3(setBucket)} -f`); + }); + + describe('buckets set', () => { + it('should set --access public', () => { + const result = runCli( + `buckets set ${setBucket} --access public` + ); + expect(result.exitCode).toBe(0); + // Reset back + runCli(`buckets set ${setBucket} --access private`); + }); + + it('should set --cache-control "max-age=3600"', () => { + const result = runCli( + `buckets set ${setBucket} --cache-control "max-age=3600"` + ); + expect(result.exitCode).toBe(0); + }); + + it('should set --enable-delete-protection true', () => { + const result = runCli( + `buckets set ${setBucket} --enable-delete-protection true` + ); + expect(result.exitCode).toBe(0); + // Disable for cleanup + runCli( + `buckets set ${setBucket} --enable-delete-protection false` + ); + }); + + it('should set --locations usa', () => { + const result = runCli( + `buckets set ${setBucket} --locations usa` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error when no settings provided', () => { + const result = runCli(`buckets set ${setBucket}`); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('At least one setting is required'); + }); + + it('should error without bucket name', () => { + const result = runCli('buckets set --access public'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("missing required argument 'name'"); + }); + }); + + describe('buckets set-ttl', () => { + it('should set TTL with --days 30', () => { + 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' + ); + }); + }); + + describe('buckets set-locations', () => { + it('should set locations with --locations usa', () => { + const result = runCli( + `buckets set-locations ${setBucket} --locations usa` + ); + expect(result.exitCode).toBe(0); + }); + }); + + describe('buckets set-migration', () => { + it('should disable migration', () => { + const result = runCli( + `buckets set-migration ${setBucket} --disable` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error on --disable with other options', () => { + const result = runCli( + `buckets set-migration ${setBucket} --disable --bucket other` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Cannot use --disable with other migration options' + ); + }); + + it('should error when missing required params', () => { + const result = runCli( + `buckets set-migration ${setBucket} --bucket other` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Required:'); + }); + }); + + describe('buckets set-transition', () => { + it('should set with --days 30 --storage-class GLACIER', () => { + 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'); + }); + }); + + describe('buckets set-notifications', () => { + it('should enable with --url', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --url https://example.com/webhook` + ); + expect(result.exitCode).toBe(0); + }); + + it('should disable', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --disable` + ); + expect(result.exitCode).toBe(0); + }); + + it('should reset', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --reset` + ); + expect(result.exitCode).toBe(0); + }); + + it('should accept --token auth', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --url https://example.com/webhook --token my-secret-token` + ); + expect(result.exitCode).toBe(0); + }); + + it('should accept --username/--password auth', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --url https://example.com/webhook --username user1 --password pass1` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error on multiple action flags', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --enable --disable` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Only one of --enable, --disable, or --reset can be used' + ); + }); + + it('should error on --reset with other options', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --reset --url https://example.com` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Cannot use --reset with other options' + ); + }); + + it('should error on --token with --username', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --url https://example.com --token tok --username user` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Cannot use --token with --username/--password' + ); + }); + + it('should error on --username without --password', () => { + const result = runCli( + `buckets set-notifications ${setBucket} --url https://example.com --username user` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Both --username and --password are required' + ); + }); + + it('should error when no options provided', () => { + const result = runCli( + `buckets set-notifications ${setBucket}` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Provide at least one option'); + }); + }); + + describe('buckets set-cors', () => { + it('should set with --origins and --methods', () => { + const result = runCli( + `buckets set-cors ${setBucket} --origins "*" --methods "GET,POST"` + ); + expect(result.exitCode).toBe(0); + }); + + it('should reset with --reset', () => { + const result = runCli( + `buckets set-cors ${setBucket} --reset` + ); + expect(result.exitCode).toBe(0); + }); + + it('should set with --override', () => { + const result = runCli( + `buckets set-cors ${setBucket} --origins "*" --override` + ); + expect(result.exitCode).toBe(0); + }); + + it('should error on --reset with other options', () => { + const result = runCli( + `buckets set-cors ${setBucket} --reset --origins "*"` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + 'Cannot use --reset with other options' + ); + }); + + it('should error without --origins or --reset', () => { + const result = runCli( + `buckets set-cors ${setBucket} --methods "GET"` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Provide --origins or --reset'); + }); + + it('should error on invalid --max-age', () => { + const result = runCli( + `buckets set-cors ${setBucket} --origins "*" --max-age -1` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain( + '--max-age must be a positive number' + ); + }); + }); + }); + + describe('objects delete command', () => { + it('should delete a single object with --force', () => { + runCli(`touch ${testBucket}/objdel-1.txt`); + const result = runCli( + `objects delete ${testBucket} objdel-1.txt --force` + ); + expect(result.exitCode).toBe(0); + }); + + it('should delete multiple objects with --force', () => { + runCli(`touch ${testBucket}/objdel-2.txt`); + runCli(`touch ${testBucket}/objdel-3.txt`); + const result = runCli( + `objects delete ${testBucket} objdel-2.txt,objdel-3.txt --force` + ); + expect(result.exitCode).toBe(0); + }); + + it('should fail without --force in non-TTY', () => { + runCli(`touch ${testBucket}/objdel-noforce.txt`); + const result = runCli( + `objects delete ${testBucket} objdel-noforce.txt` + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('--yes'); + // Cleanup + runCli(`objects delete ${testBucket} objdel-noforce.txt --force`); + }); + }); + + describe('objects set command', () => { + beforeAll(() => { + runCli(`touch ${testBucket}/objset-test.txt`); + }); + + afterAll(() => { + // The object may have been renamed + runCli(`rm ${t3(testBucket)}/objset-test.txt -f`); + runCli(`rm ${t3(testBucket)}/objset-renamed.txt -f`); + }); + + it('should set --access public', () => { + const result = runCli( + `objects set ${testBucket} objset-test.txt --access public` + ); + expect(result.exitCode).toBe(0); + }); + + it('should set --access private', () => { + const result = runCli( + `objects set ${testBucket} objset-test.txt --access private` + ); + expect(result.exitCode).toBe(0); + }); + + it('should rename with --new-key', () => { + const result = runCli( + `objects set ${testBucket} objset-test.txt --access private --new-key objset-renamed.txt` + ); + expect(result.exitCode).toBe(0); + + // Verify rename + const ls = runCli(`ls ${testBucket}`); + expect(ls.stdout).toContain('objset-renamed.txt'); + }); + }); + + describe('snapshot and fork lifecycle', () => { + const snapBucket = `${testPrefix}-snap`; + const forkBucket = `${testPrefix}-fork`; + let snapshotVersion: string; + + beforeAll(() => { + runCli(`mk ${snapBucket} --enable-snapshots`); + runCli(`touch ${snapBucket}/snap-file.txt`); + }); + + afterAll(() => { + runCli(`rm ${t3(forkBucket)} -f`); + runCli(`rm ${t3(snapBucket)}/snap-file.txt -f`); + runCli(`rm ${t3(snapBucket)} -f`); + }); + + it('should take a snapshot', () => { + const result = runCli(`snapshots take ${snapBucket}`); + expect(result.exitCode).toBe(0); + }); + + it('should take a named snapshot with --snapshot-name', () => { + const result = runCli( + `snapshots take ${snapBucket} test-snap` + ); + expect(result.exitCode).toBe(0); + }); + + it('should list snapshots', () => { + const result = runCli(`snapshots list ${snapBucket}`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Version'); + }); + + it('should list snapshots with --format json', () => { + const result = runCli( + `snapshots list ${snapBucket} --format json` + ); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout.trim()); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + // Save version for later tests + snapshotVersion = parsed[0].version; + expect(snapshotVersion).toBeTruthy(); + }); + + it('should ls with --snapshot-version', () => { + const result = runCli( + `ls ${snapBucket} --snapshot-version ${snapshotVersion}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('snap-file.txt'); + }); + + it('should objects list with --snapshot-version', () => { + const result = runCli( + `objects list ${snapBucket} --snapshot-version ${snapshotVersion}` + ); + expect(result.exitCode).toBe(0); + }); + + it('should stat object with --snapshot-version', () => { + const result = runCli( + `stat ${snapBucket}/snap-file.txt --snapshot-version ${snapshotVersion}` + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Size'); + }); + + it('should create a fork via forks create', () => { + const result = runCli( + `forks create ${snapBucket} ${forkBucket}` + ); + 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` + ); + 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(); + } + }, 120_000); + + it('should list forks via buckets list --forks-of', () => { + // Retry — fork visibility is eventually consistent + let result = { stdout: '', stderr: '', exitCode: 1 }; + for (let i = 0; i < 3; i++) { + result = runCli(`buckets list --forks-of ${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); + }); + + describe('credentials test command', () => { + it('should verify credentials (no bucket)', () => { + const result = runCli('credentials test'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Access verified'); + }); + + it('should verify credentials for specific bucket', () => { + const result = runCli(`credentials test --bucket ${testBucket}`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Access verified'); }); }); }); diff --git a/test/specs-completeness.test.ts b/test/specs-completeness.test.ts new file mode 100644 index 0000000..0f5d12f --- /dev/null +++ b/test/specs-completeness.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect } from 'vitest'; +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; +import * as YAML from 'yaml'; +import { setSpecs, loadSpecs } from '../src/utils/specs.js'; +import type { CommandSpec, Specs } from '../src/types.js'; + +interface LeafCommand { + spec: CommandSpec; + path: string[]; +} + +/** + * Recursively walk the spec tree and collect all leaf commands. + * A leaf is a command with no children, OR whose only role is as a + * default target of its parent (in which case the parent routes to it). + */ +function collectLeaves( + commands: CommandSpec[], + parentPath: string[] = [] +): LeafCommand[] { + const leaves: LeafCommand[] = []; + + for (const cmd of commands) { + const currentPath = [...parentPath, cmd.name]; + + if (!cmd.commands || cmd.commands.length === 0) { + // No children → leaf + leaves.push({ spec: cmd, path: currentPath }); + } else { + // Has children → recurse into them + leaves.push(...collectLeaves(cmd.commands, currentPath)); + } + } + + return leaves; +} + +/** + * Recursively collect ALL commands (not just leaves) for structural checks. + */ +function collectAllCommands( + commands: CommandSpec[], + parentPath: string[] = [] +): LeafCommand[] { + const all: LeafCommand[] = []; + + for (const cmd of commands) { + const currentPath = [...parentPath, cmd.name]; + all.push({ spec: cmd, path: currentPath }); + + if (cmd.commands && cmd.commands.length > 0) { + all.push(...collectAllCommands(cmd.commands, currentPath)); + } + } + + return all; +} + +const srcRoot = join(process.cwd(), 'src', 'lib'); + +// Pre-populate specs cache from source YAML so we don't need dist/ +const specsYaml = readFileSync(join(process.cwd(), 'src', 'specs.yaml'), 'utf8'); +setSpecs(YAML.parse(specsYaml, { schema: 'core' }) as Specs); + +describe('specs completeness', () => { + const specs = loadSpecs(); + const leaves = collectLeaves(specs.commands); + const allCommands = collectAllCommands(specs.commands); + + it('found leaf commands to validate', () => { + expect(leaves.length).toBeGreaterThan(0); + }); + + describe('every leaf command has a handler file', () => { + for (const { path } of leaves) { + const label = path.join(' '); + it(`${label}`, () => { + const filePath = join(srcRoot, ...path) + '.ts'; + const indexPath = join(srcRoot, ...path, 'index.ts'); + const exists = existsSync(filePath) || existsSync(indexPath); + expect(exists, `Missing handler: ${filePath} or ${indexPath}`).toBe( + true + ); + }); + } + }); + + describe('every leaf command has a messages block', () => { + for (const { spec, path } of leaves) { + const label = path.join(' '); + it(`${label}`, () => { + expect( + spec.messages, + `${label} is missing a messages block` + ).toBeDefined(); + }); + } + }); + + describe('no duplicate argument names within a command', () => { + for (const { spec, path } of allCommands) { + if (!spec.arguments || spec.arguments.length === 0) continue; + const label = path.join(' '); + it(`${label}`, () => { + const names = spec.arguments!.map((a) => a.name); + expect(names.length).toBe(new Set(names).size); + }); + } + }); + + describe('no alias collisions within a command', () => { + for (const { spec, path } of allCommands) { + if (!spec.arguments || spec.arguments.length === 0) continue; + const label = path.join(' '); + it(`${label}`, () => { + const names = spec.arguments!.map((a) => a.name); + const aliases = spec.arguments! + .filter((a) => a.alias) + .map((a) => a.alias as string); + + // No alias should match another arg's name + for (const alias of aliases) { + // An alias matching its own arg's name is fine (long alias pattern), + // but it shouldn't match a *different* arg's name + const argsWithThisAlias = spec.arguments!.filter( + (a) => a.alias === alias + ); + const otherNames = names.filter( + (n) => !argsWithThisAlias.some((a) => a.name === n) && n === alias + ); + expect( + otherNames.length, + `Alias "${alias}" collides with arg name in ${label}` + ).toBe(0); + } + + // No two aliases should be the same + expect(aliases.length).toBe(new Set(aliases).size); + }); + } + }); + + describe('deprecated commands have onDeprecated message', () => { + const deprecated = allCommands.filter(({ spec }) => spec.deprecated); + + if (deprecated.length === 0) { + it('no deprecated commands found (skip)', () => { + expect(true).toBe(true); + }); + } + + for (const { spec, path } of deprecated) { + const label = path.join(' '); + it(`${label}`, () => { + expect( + spec.messages?.onDeprecated, + `Deprecated command ${label} is missing onDeprecated message` + ).toBeDefined(); + }); + } + }); +}); diff --git a/test/utils/format.test.ts b/test/utils/format.test.ts new file mode 100644 index 0000000..7e2938e --- /dev/null +++ b/test/utils/format.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect } from 'vitest'; +import { + formatSize, + formatJson, + formatXml, + formatXmlObject, + formatTable, + formatOutput, + type TableColumn, +} from '../../src/utils/format.js'; + +describe('formatSize', () => { + it.each([ + [0, '0 B'], + [500, '500 B'], + [1024, '1.0 KB'], + [1536, '1.5 KB'], + [1048576, '1.0 MB'], + [1073741824, '1.0 GB'], + [1099511627776, '1.0 TB'], + ])('formatSize(%d) → %s', (bytes, expected) => { + expect(formatSize(bytes)).toBe(expected); + }); +}); + +describe('formatJson', () => { + it('formats object with 2-space indent', () => { + const result = formatJson({ name: 'test', count: 42 }); + expect(result).toBe(JSON.stringify({ name: 'test', count: 42 }, null, 2)); + }); + + it('formats array', () => { + const result = formatJson([1, 2, 3]); + expect(result).toBe(JSON.stringify([1, 2, 3], null, 2)); + }); +}); + +describe('formatXml', () => { + it('wraps items in root and item tags', () => { + const items = [{ name: 'test', size: 100 }]; + const result = formatXml(items, 'Buckets', 'Bucket'); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('test'); + expect(result).toContain('100'); + }); + + it('escapes special XML characters', () => { + const items = [{ text: '& < > " \'' }]; + const result = formatXml(items, 'Root', 'Item'); + expect(result).toContain('&'); + expect(result).toContain('<'); + expect(result).toContain('>'); + expect(result).toContain('"'); + expect(result).toContain('''); + }); +}); + +describe('formatXmlObject', () => { + it('formats object fields as XML elements', () => { + const result = formatXmlObject({ key: 'value' }, ' '); + expect(result).toBe(' value'); + }); +}); + +describe('formatTable', () => { + const columns: TableColumn[] = [ + { key: 'name', header: 'Name' }, + { key: 'size', header: 'Size' }, + ]; + + it('contains box-drawing characters', () => { + const items = [{ name: 'test', size: '1 KB' }]; + const result = formatTable(items, columns); + expect(result).toContain('┌'); + expect(result).toContain('─'); + expect(result).toContain('┬'); + expect(result).toContain('┐'); + expect(result).toContain('│'); + expect(result).toContain('├'); + expect(result).toContain('┼'); + expect(result).toContain('┤'); + expect(result).toContain('└'); + expect(result).toContain('┴'); + expect(result).toContain('┘'); + }); + + it('includes header row with column names', () => { + const items = [{ name: 'test', size: '1 KB' }]; + const result = formatTable(items, columns); + expect(result).toContain('Name'); + expect(result).toContain('Size'); + }); + + it('includes data values', () => { + const items = [{ name: 'my-bucket', size: '42 MB' }]; + const result = formatTable(items, columns); + expect(result).toContain('my-bucket'); + expect(result).toContain('42 MB'); + }); + + it('right-aligns when specified', () => { + const cols: TableColumn[] = [ + { key: 'name', header: 'Name' }, + { key: 'count', header: 'Count', align: 'right' }, + ]; + const items = [{ name: 'a', count: '5' }]; + const result = formatTable(items, cols); + // The right-aligned cell should have leading spaces before the value + const lines = result.split('\n'); + const dataLine = lines.find( + (l) => l.includes('│') && l.includes('5') && !l.includes('Count') + ); + expect(dataLine).toBeDefined(); + // In right-align, "5" is padStart'd, so the cell content ends with "5 │" + // meaning there are spaces before "5" and the value is right-justified + const cells = dataLine!.split('│'); + const countCell = cells[2]; // space + value + space + // Right-aligned: value should be at the end of the cell (after trimming the border space) + const trimmed = countCell.slice(1, -1); // remove border padding spaces + expect(trimmed).toBe('5'.padStart(trimmed.length)); + }); + + it('renders header even with empty items', () => { + const result = formatTable([], columns); + expect(result).toContain('Name'); + expect(result).toContain('Size'); + expect(result).toContain('┌'); + expect(result).toContain('┘'); + }); +}); + +describe('formatOutput', () => { + const columns: TableColumn[] = [ + { key: 'name', header: 'Name' }, + ]; + const items = [{ name: 'test' }]; + + it("'json' → JSON output", () => { + const result = formatOutput(items, 'json', 'Root', 'Item', columns); + expect(JSON.parse(result)).toEqual(items); + }); + + it("'xml' → XML output", () => { + const result = formatOutput(items, 'xml', 'Root', 'Item', columns); + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it("'table' → table output", () => { + const result = formatOutput(items, 'table', 'Root', 'Item', columns); + expect(result).toContain('┌'); + expect(result).toContain('Name'); + }); + + it('default format → table output', () => { + const result = formatOutput(items, 'anything', 'Root', 'Item', columns); + expect(result).toContain('┌'); + }); +}); diff --git a/test/utils/locations.test.ts b/test/utils/locations.test.ts new file mode 100644 index 0000000..5da0081 --- /dev/null +++ b/test/utils/locations.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { parseLocations } from '../../src/utils/locations.js'; + +describe('parseLocations', () => { + it("'global' → {type: 'global'}", () => { + expect(parseLocations('global')).toEqual({ type: 'global' }); + }); + + it("'' → {type: 'global'}", () => { + expect(parseLocations('')).toEqual({ type: 'global' }); + }); + + it("[] → {type: 'global'}", () => { + expect(parseLocations([])).toEqual({ type: 'global' }); + }); + + it("'usa' → multi region", () => { + expect(parseLocations('usa')).toEqual({ type: 'multi', values: 'usa' }); + }); + + it("'eur' → multi region", () => { + expect(parseLocations('eur')).toEqual({ type: 'multi', values: 'eur' }); + }); + + it("'ams' → single region", () => { + expect(parseLocations('ams')).toEqual({ type: 'single', values: 'ams' }); + }); + + it("'sjc' → single region", () => { + expect(parseLocations('sjc')).toEqual({ type: 'single', values: 'sjc' }); + }); + + it("'ams,fra' → dual region", () => { + expect(parseLocations('ams,fra')).toEqual({ + type: 'dual', + values: ['ams', 'fra'], + }); + }); + + it("['ams', 'fra'] → dual region", () => { + expect(parseLocations(['ams', 'fra'])).toEqual({ + type: 'dual', + values: ['ams', 'fra'], + }); + }); + + it("trims whitespace: ' ams , fra '", () => { + expect(parseLocations(' ams , fra ')).toEqual({ + type: 'dual', + values: ['ams', 'fra'], + }); + }); + + it("flattens: ['ams,fra', 'sjc']", () => { + expect(parseLocations(['ams,fra', 'sjc'])).toEqual({ + type: 'dual', + values: ['ams', 'fra', 'sjc'], + }); + }); +}); diff --git a/test/utils/messages.test.ts b/test/utils/messages.test.ts new file mode 100644 index 0000000..5827288 --- /dev/null +++ b/test/utils/messages.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import * as YAML from 'yaml'; +import { setSpecs } from '../../src/utils/specs.js'; +import { + printFailure, + printDeprecated, + printStart, + printSuccess, + printEmpty, + printAlreadyDone, + printHint, +} from '../../src/utils/messages.js'; + +// Save original descriptor so we can restore it +const originalIsTTY = Object.getOwnPropertyDescriptor( + process.stdout, + 'isTTY' +); + +function setTTY(value: boolean) { + Object.defineProperty(process.stdout, 'isTTY', { + value, + writable: true, + configurable: true, + }); +} + +function restoreTTY() { + if (originalIsTTY) { + Object.defineProperty(process.stdout, 'isTTY', originalIsTTY); + } else { + delete (process.stdout as unknown as Record).isTTY; + } +} + +// Pre-populate specs cache from source YAML so we don't need dist/ +const specsYaml = readFileSync(join(process.cwd(), 'src', 'specs.yaml'), 'utf8'); +setSpecs(YAML.parse(specsYaml, { schema: 'core' })); + +describe('messages', () => { + let logSpy: ReturnType; + let errorSpy: ReturnType; + let warnSpy: ReturnType; + + beforeEach(() => { + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + warnSpy.mockRestore(); + restoreTTY(); + }); + + // Use a real command from specs for testing + const ctx = { command: 'buckets', operation: 'create' }; + + describe('printFailure', () => { + it('prints error with ✖ prefix (TTY)', () => { + setTTY(true); + printFailure(ctx, 'something went wrong'); + expect(errorSpy).toHaveBeenCalled(); + const allArgs = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allArgs).toContain('✖'); + expect(allArgs).toContain('something went wrong'); + }); + + it('prints error even when not TTY', () => { + setTTY(false); + printFailure(ctx, 'something went wrong'); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + describe('printDeprecated', () => { + it('prints ⚠ Deprecated when TTY', () => { + setTTY(true); + printDeprecated('use new-command instead'); + expect(warnSpy).toHaveBeenCalled(); + expect(warnSpy.mock.calls[0][0]).toContain('⚠ Deprecated:'); + expect(warnSpy.mock.calls[0][0]).toContain('use new-command instead'); + }); + + it('is silent when not TTY', () => { + setTTY(false); + printDeprecated('use new-command instead'); + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); + + describe('printStart', () => { + it('prints onStart message when TTY', () => { + setTTY(true); + printStart(ctx); + expect(logSpy).toHaveBeenCalled(); + }); + + it('is silent when not TTY', () => { + setTTY(false); + printStart(ctx); + expect(logSpy).not.toHaveBeenCalled(); + }); + }); + + describe('printSuccess', () => { + it('prints ✔ prefix when TTY', () => { + setTTY(true); + printSuccess(ctx); + expect(logSpy).toHaveBeenCalled(); + const output = logSpy.mock.calls[0][0] as string; + expect(output).toContain('✔'); + }); + + it('is silent when not TTY', () => { + setTTY(false); + printSuccess(ctx); + expect(logSpy).not.toHaveBeenCalled(); + }); + }); + + describe('printEmpty', () => { + // Use a command that has onEmpty message + const emptyCtx = { command: 'buckets', operation: 'list' }; + + it('prints when TTY', () => { + setTTY(true); + printEmpty(emptyCtx); + // Will only print if the spec has an onEmpty message + // Either way, it should not throw + }); + + it('is silent when not TTY', () => { + setTTY(false); + printEmpty(emptyCtx); + expect(logSpy).not.toHaveBeenCalled(); + }); + }); + + describe('printAlreadyDone', () => { + it('is silent when not TTY', () => { + setTTY(false); + printAlreadyDone(ctx); + expect(logSpy).not.toHaveBeenCalled(); + }); + }); + + describe('printHint', () => { + it('is silent when not TTY', () => { + setTTY(false); + printHint(ctx); + expect(logSpy).not.toHaveBeenCalled(); + }); + }); + + describe('variable interpolation', () => { + it('replaces {{name}} in output', () => { + setTTY(true); + // buckets create onSuccess is "Bucket '{{name}}' created" + printSuccess(ctx, { name: 'my-bucket' }); + expect(logSpy).toHaveBeenCalled(); + const output = logSpy.mock.calls[0][0] as string; + expect(output).toContain('my-bucket'); + expect(output).not.toContain('{{name}}'); + }); + }); +}); diff --git a/test/utils/specs.test.ts b/test/utils/specs.test.ts index fbfa961..8b964a5 100644 --- a/test/utils/specs.test.ts +++ b/test/utils/specs.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { getCommandSpec, getArgumentSpec } from '../../dist/utils/specs.js'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import * as YAML from 'yaml'; +import { setSpecs, getCommandSpec, getArgumentSpec } from '../../src/utils/specs.js'; + +// Pre-populate specs cache from source YAML so we don't need dist/ +const specsYaml = readFileSync(join(process.cwd(), 'src', 'specs.yaml'), 'utf8'); +setSpecs(YAML.parse(specsYaml, { schema: 'core' })); describe('getCommandSpec', () => { describe('top-level commands', () => { From d628663a937bf23a70603ac03da15c98cbb0fa93 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Wed, 18 Mar 2026 07:43:38 +0000 Subject: [PATCH 6/7] feat: suggest next actions (#40) * feat: suggest next actions --- src/cli-core.ts | 15 +- src/global.d.ts | 4 + src/lib/access-keys/assign.ts | 53 ++++-- src/lib/access-keys/create.ts | 40 ++-- src/lib/access-keys/delete.ts | 15 +- src/lib/access-keys/get.ts | 15 +- src/lib/access-keys/list.ts | 13 +- src/lib/buckets/create.ts | 25 ++- src/lib/buckets/delete.ts | 20 +- src/lib/buckets/get.ts | 5 +- src/lib/buckets/list.ts | 7 +- src/lib/buckets/set-cors.ts | 11 +- src/lib/buckets/set-locations.ts | 7 +- src/lib/buckets/set-migration.ts | 14 +- src/lib/buckets/set-notifications.ts | 21 ++- src/lib/buckets/set-transition.ts | 31 +++- src/lib/buckets/set-ttl.ts | 18 +- src/lib/buckets/set.ts | 7 +- src/lib/configure/index.ts | 8 +- src/lib/cp.ts | 62 +++---- src/lib/credentials/test.ts | 10 +- src/lib/forks/create.ts | 7 +- src/lib/forks/list.ts | 7 +- src/lib/iam/policies/create.ts | 27 ++- src/lib/iam/policies/delete.ts | 15 +- src/lib/iam/policies/edit.ts | 28 ++- src/lib/iam/policies/get.ts | 15 +- src/lib/iam/policies/list.ts | 13 +- src/lib/iam/users/invite.ts | 20 +- src/lib/iam/users/list.ts | 13 +- src/lib/iam/users/remove.ts | 15 +- src/lib/iam/users/revoke-invitation.ts | 15 +- src/lib/iam/users/update-role.ts | 30 ++- src/lib/login/credentials.ts | 4 +- src/lib/login/oauth.ts | 7 +- src/lib/logout.ts | 3 +- src/lib/ls.ts | 10 +- src/lib/mk.ts | 16 +- src/lib/mv.ts | 33 ++-- src/lib/objects/delete.ts | 25 ++- src/lib/objects/get.ts | 9 +- src/lib/objects/list.ts | 5 +- src/lib/objects/put.ts | 12 +- src/lib/objects/set.ts | 12 +- src/lib/organizations/create.ts | 6 +- src/lib/organizations/list.ts | 3 +- src/lib/organizations/select.ts | 8 +- src/lib/presign.ts | 31 ++-- src/lib/rm.ts | 24 +-- src/lib/snapshots/list.ts | 5 +- src/lib/snapshots/take.ts | 5 +- src/lib/stat.ts | 11 +- src/lib/touch.ts | 13 +- src/lib/whoami.ts | 5 +- src/specs.yaml | 41 +++++ src/types.ts | 6 + src/utils/errors.ts | 181 +++++++++++++++++++ src/utils/exit.ts | 90 +++++++++ src/utils/messages.ts | 7 +- test/specs-completeness.test.ts | 32 ++++ test/utils/errors.test.ts | 216 ++++++++++++++++++++++ test/utils/exit.test.ts | 241 +++++++++++++++++++++++++ 62 files changed, 1347 insertions(+), 320 deletions(-) create mode 100644 src/utils/errors.ts create mode 100644 src/utils/exit.ts create mode 100644 test/utils/errors.test.ts create mode 100644 test/utils/exit.test.ts diff --git a/src/cli-core.ts b/src/cli-core.ts index a19d932..bfcd2f8 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -5,6 +5,7 @@ import { Command as CommanderCommand } from 'commander'; import type { Argument, CommandSpec, Specs } from './types.js'; import { printDeprecated } from './utils/messages.js'; +import { exitWithError } from './utils/exit.js'; export interface ModuleLoader { (commandPath: string[]): Promise<{ @@ -33,16 +34,11 @@ export function setupErrorHandlers() { console.error('\nOperation cancelled'); process.exit(1); } - console.error( - '\nError:', - reason instanceof Error ? reason.message : reason - ); - process.exit(1); + exitWithError(reason); }); process.on('uncaughtException', (error) => { - console.error('\nError:', error.message); - process.exit(1); + exitWithError(error); }); } @@ -369,6 +365,11 @@ async function loadAndExecuteCommand( positionalArgs: string[] = [], options: Record = {} ) { + // Set JSON mode globally for error handlers + if (options.json || options.format === 'json') { + globalThis.__TIGRIS_JSON_MODE = true; + } + const { module, error: loadError } = await loadModule(pathParts); if (loadError || !module) { diff --git a/src/global.d.ts b/src/global.d.ts index b680f2c..727ff22 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -2,3 +2,7 @@ declare module '*.yaml' { const content: string; export default content; } + +// Global JSON mode flag set by CLI core when --json or --format=json is used +// eslint-disable-next-line no-var +declare var __TIGRIS_JSON_MODE: boolean | undefined; diff --git a/src/lib/access-keys/assign.ts b/src/lib/access-keys/assign.ts index 5b388e3..c35bdc1 100644 --- a/src/lib/access-keys/assign.ts +++ b/src/lib/access-keys/assign.ts @@ -10,6 +10,11 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../utils/exit.js'; const context = msg('access-keys', 'assign'); @@ -44,12 +49,12 @@ export default async function assign(options: Record) { if (!id) { printFailure(context, 'Access key ID is required'); - process.exit(1); + exitWithError('Access key ID is required', context); } if (admin && revokeRoles) { printFailure(context, 'Cannot use --admin and --revoke-roles together'); - process.exit(1); + exitWithError('Cannot use --admin and --revoke-roles together', context); } const loginMethod = await getLoginMethod(); @@ -59,7 +64,10 @@ export default async function assign(options: Record) { context, 'Bucket roles can only be managed when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Bucket roles can only be managed when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -67,7 +75,10 @@ export default async function assign(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -85,7 +96,7 @@ export default async function assign(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { @@ -107,7 +118,10 @@ export default async function assign(options: Record) { context, 'At least one bucket name is required (or use --admin or --revoke-roles)' ); - process.exit(1); + exitWithError( + 'At least one bucket name is required (or use --admin or --revoke-roles)', + context + ); } if (roles.length === 0) { @@ -115,7 +129,10 @@ export default async function assign(options: Record) { context, 'At least one role is required (or use --admin or --revoke-roles)' ); - process.exit(1); + exitWithError( + 'At least one role is required (or use --admin or --revoke-roles)', + context + ); } // Validate all roles @@ -125,7 +142,10 @@ export default async function assign(options: Record) { context, `Invalid role "${role}". Valid roles are: ${validRoles.join(', ')}` ); - process.exit(1); + exitWithError( + `Invalid role "${role}". Valid roles are: ${validRoles.join(', ')}`, + context + ); } } @@ -147,7 +167,10 @@ export default async function assign(options: Record) { context, `Number of roles (${roles.length}) must be 1 or match number of buckets (${buckets.length})` ); - process.exit(1); + exitWithError( + `Number of roles (${roles.length}) must be 1 or match number of buckets (${buckets.length})`, + context + ); } } @@ -155,12 +178,20 @@ export default async function assign(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { - console.log(JSON.stringify({ action: 'assigned', id, assignments })); + const nextActions = getSuccessNextActions(context); + const output: Record = { + action: 'assigned', + id, + assignments, + }; + if (nextActions.length > 0) output.nextActions = nextActions; + console.log(JSON.stringify(output)); } printSuccess(context); + printNextActions(context); } diff --git a/src/lib/access-keys/create.ts b/src/lib/access-keys/create.ts index 2dbe5fe..3d06c85 100644 --- a/src/lib/access-keys/create.ts +++ b/src/lib/access-keys/create.ts @@ -10,6 +10,11 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../utils/exit.js'; const context = msg('access-keys', 'create'); @@ -25,7 +30,7 @@ export default async function create(options: Record) { if (!name) { printFailure(context, 'Access key name is required'); - process.exit(1); + exitWithError('Access key name is required', context); } const loginMethod = await getLoginMethod(); @@ -35,7 +40,10 @@ export default async function create(options: Record) { context, 'Access keys can only be created when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Access keys can only be created when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -43,7 +51,10 @@ export default async function create(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -60,18 +71,22 @@ export default async function create(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { - console.log( - JSON.stringify({ - action: 'created', - name: data.name, - id: data.id, - secret: data.secret, - }) - ); + const nextActions = getSuccessNextActions(context, { + name: data.name, + id: data.id, + }); + const output: Record = { + action: 'created', + name: data.name, + id: data.id, + secret: data.secret, + }; + if (nextActions.length > 0) output.nextActions = nextActions; + console.log(JSON.stringify(output)); } else { console.log(` Name: ${data.name}`); console.log(` Access Key ID: ${data.id}`); @@ -83,4 +98,5 @@ export default async function create(options: Record) { } printSuccess(context); + printNextActions(context, { name: data.name, id: data.id }); } diff --git a/src/lib/access-keys/delete.ts b/src/lib/access-keys/delete.ts index 8143ba9..f5305fb 100644 --- a/src/lib/access-keys/delete.ts +++ b/src/lib/access-keys/delete.ts @@ -10,6 +10,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; import { requireInteractive, confirm } from '../../utils/interactive.js'; const context = msg('access-keys', 'delete'); @@ -27,7 +28,7 @@ export default async function remove(options: Record) { if (!id) { printFailure(context, 'Access key ID is required'); - process.exit(1); + exitWithError('Access key ID is required', context); } const loginMethod = await getLoginMethod(); @@ -37,7 +38,10 @@ export default async function remove(options: Record) { context, 'Access keys can only be deleted when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Access keys can only be deleted when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -45,7 +49,10 @@ export default async function remove(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } if (!force) { @@ -71,7 +78,7 @@ export default async function remove(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/access-keys/get.ts b/src/lib/access-keys/get.ts index 23c9129..61255f1 100644 --- a/src/lib/access-keys/get.ts +++ b/src/lib/access-keys/get.ts @@ -10,6 +10,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('access-keys', 'get'); @@ -25,7 +26,7 @@ export default async function get(options: Record) { if (!id) { printFailure(context, 'Access key ID is required'); - process.exit(1); + exitWithError('Access key ID is required', context); } const loginMethod = await getLoginMethod(); @@ -35,7 +36,10 @@ export default async function get(options: Record) { context, 'Access keys can only be retrieved when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Access keys can only be retrieved when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -43,7 +47,10 @@ export default async function get(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -60,7 +67,7 @@ export default async function get(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/access-keys/list.ts b/src/lib/access-keys/list.ts index ceddbb5..cf5be84 100644 --- a/src/lib/access-keys/list.ts +++ b/src/lib/access-keys/list.ts @@ -12,6 +12,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('access-keys', 'list'); @@ -30,7 +31,10 @@ export default async function list(options: Record) { context, 'Access keys can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Access keys can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -38,7 +42,10 @@ export default async function list(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -55,7 +62,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data.accessKeys || data.accessKeys.length === 0) { diff --git a/src/lib/buckets/create.ts b/src/lib/buckets/create.ts index f625e6f..22e6ff7 100644 --- a/src/lib/buckets/create.ts +++ b/src/lib/buckets/create.ts @@ -12,6 +12,11 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../utils/exit.js'; const { prompt } = enquirer; @@ -130,18 +135,18 @@ export default async function create(options: Record) { parsedLocations = await promptLocations(); } catch (err) { printFailure(context, (err as Error).message); - process.exit(1); + exitWithError(err, context); } } if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (sourceSnapshot && !forkOf) { printFailure(context, '--source-snapshot requires --fork-of'); - process.exit(1); + exitWithError('--source-snapshot requires --fork-of', context); } const { error } = await createBucket(name, { @@ -156,14 +161,20 @@ export default async function create(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { - console.log( - JSON.stringify({ action: 'created', name, ...(forkOf ? { forkOf } : {}) }) - ); + const nextActions = getSuccessNextActions(context, { name }); + const output: Record = { + action: 'created', + name, + ...(forkOf ? { forkOf } : {}), + }; + if (nextActions.length > 0) output.nextActions = nextActions; + console.log(JSON.stringify(output)); } printSuccess(context, { name }); + printNextActions(context, { name }); } diff --git a/src/lib/buckets/delete.ts b/src/lib/buckets/delete.ts index 7cc6520..f4ffd27 100644 --- a/src/lib/buckets/delete.ts +++ b/src/lib/buckets/delete.ts @@ -8,6 +8,11 @@ import { msg, } from '../../utils/messages.js'; import { requireInteractive, confirm } from '../../utils/interactive.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../utils/exit.js'; const context = msg('buckets', 'delete'); @@ -24,7 +29,7 @@ export default async function deleteBucket(options: Record) { if (!names) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const bucketNames = Array.isArray(names) ? names : [names]; @@ -54,10 +59,19 @@ export default async function deleteBucket(options: Record) { } if (format === 'json') { - console.log(JSON.stringify({ action: 'deleted', names: deleted, errors })); + const nextActions = getSuccessNextActions(context); + const output: Record = { + action: 'deleted', + names: deleted, + errors, + }; + if (nextActions.length > 0) output.nextActions = nextActions; + console.log(JSON.stringify(output)); } if (errors.length > 0) { - process.exit(1); + exitWithError(errors[0].error, context); } + + printNextActions(context); } diff --git a/src/lib/buckets/get.ts b/src/lib/buckets/get.ts index 48844e3..9186758 100644 --- a/src/lib/buckets/get.ts +++ b/src/lib/buckets/get.ts @@ -9,6 +9,7 @@ import { msg, } from '../../utils/messages.js'; import { buildBucketInfo } from '../../utils/bucket-info.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'get'); @@ -23,7 +24,7 @@ export default async function get(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const { data, error } = await getBucketInfo(name, { @@ -32,7 +33,7 @@ export default async function get(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const info = [ diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index 8391a6b..5fd362a 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -9,6 +9,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'list'); @@ -27,7 +28,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data.buckets || data.buckets.length === 0) { @@ -44,7 +45,7 @@ export default async function list(options: Record) { if (infoError) { printFailure(context, infoError.message); - process.exit(1); + exitWithError(infoError, context); } if (!bucketInfo.hasForks) { @@ -95,6 +96,6 @@ export default async function list(options: Record) { } else { printFailure(context, 'An unknown error occurred'); } - process.exit(1); + exitWithError(error, context); } } diff --git a/src/lib/buckets/set-cors.ts b/src/lib/buckets/set-cors.ts index a3067b4..4fe3f35 100644 --- a/src/lib/buckets/set-cors.ts +++ b/src/lib/buckets/set-cors.ts @@ -8,6 +8,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-cors'); @@ -28,7 +29,7 @@ export default async function setCors(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if ( @@ -41,17 +42,17 @@ export default async function setCors(options: Record) { override) ) { printFailure(context, 'Cannot use --reset with other options'); - process.exit(1); + exitWithError('Cannot use --reset with other options', context); } if (!reset && !origins) { printFailure(context, 'Provide --origins or --reset'); - process.exit(1); + exitWithError('Provide --origins or --reset', context); } if (maxAge !== undefined && (isNaN(Number(maxAge)) || Number(maxAge) <= 0)) { printFailure(context, '--max-age must be a positive number'); - process.exit(1); + exitWithError('--max-age must be a positive number', context); } const config = await getStorageConfig(); @@ -81,7 +82,7 @@ export default async function setCors(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set-locations.ts b/src/lib/buckets/set-locations.ts index 6720032..4cf0d1b 100644 --- a/src/lib/buckets/set-locations.ts +++ b/src/lib/buckets/set-locations.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-locations'); @@ -22,7 +23,7 @@ export default async function setLocations(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } let parsedLocations: BucketLocations; @@ -34,7 +35,7 @@ export default async function setLocations(options: Record) { parsedLocations = await promptLocations(); } catch (err) { printFailure(context, (err as Error).message); - process.exit(1); + exitWithError(err, context); } } @@ -54,7 +55,7 @@ export default async function setLocations(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set-migration.ts b/src/lib/buckets/set-migration.ts index 7fad72e..6e79bde 100644 --- a/src/lib/buckets/set-migration.ts +++ b/src/lib/buckets/set-migration.ts @@ -8,6 +8,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-migration'); @@ -28,7 +29,7 @@ export default async function setMigration(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if ( @@ -41,7 +42,7 @@ export default async function setMigration(options: Record) { writeThrough !== undefined) ) { printFailure(context, 'Cannot use --disable with other migration options'); - process.exit(1); + exitWithError('Cannot use --disable with other migration options', context); } const config = await getStorageConfig(); @@ -61,7 +62,7 @@ export default async function setMigration(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); @@ -73,7 +74,10 @@ export default async function setMigration(options: Record) { context, 'Required: --bucket, --endpoint, --region, --access-key, --secret-key' ); - process.exit(1); + exitWithError( + 'Required: --bucket, --endpoint, --region, --access-key, --secret-key', + context + ); } const { error } = await setBucketMigration(name, { @@ -91,7 +95,7 @@ export default async function setMigration(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set-notifications.ts b/src/lib/buckets/set-notifications.ts index 69dece0..dc94926 100644 --- a/src/lib/buckets/set-notifications.ts +++ b/src/lib/buckets/set-notifications.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-notifications'); @@ -31,7 +32,7 @@ export default async function setNotifications( if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const flagCount = [enable, disable, reset].filter(Boolean).length; @@ -40,7 +41,10 @@ export default async function setNotifications( context, 'Only one of --enable, --disable, or --reset can be used' ); - process.exit(1); + exitWithError( + 'Only one of --enable, --disable, or --reset can be used', + context + ); } if ( @@ -52,7 +56,7 @@ export default async function setNotifications( password !== undefined) ) { printFailure(context, 'Cannot use --reset with other options'); - process.exit(1); + exitWithError('Cannot use --reset with other options', context); } if ( @@ -66,7 +70,7 @@ export default async function setNotifications( password === undefined ) { printFailure(context, 'Provide at least one option'); - process.exit(1); + exitWithError('Provide at least one option', context); } if (token && (username !== undefined || password !== undefined)) { @@ -74,7 +78,10 @@ export default async function setNotifications( context, 'Cannot use --token with --username/--password. Choose one auth method' ); - process.exit(1); + exitWithError( + 'Cannot use --token with --username/--password. Choose one auth method', + context + ); } if ( @@ -82,7 +89,7 @@ export default async function setNotifications( (username === undefined && password !== undefined) ) { printFailure(context, 'Both --username and --password are required'); - process.exit(1); + exitWithError('Both --username and --password are required', context); } const config = await getStorageConfig(); @@ -123,7 +130,7 @@ export default async function setNotifications( if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set-transition.ts b/src/lib/buckets/set-transition.ts index cb13355..ad92439 100644 --- a/src/lib/buckets/set-transition.ts +++ b/src/lib/buckets/set-transition.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-transition'); @@ -31,12 +32,12 @@ export default async function setTransitions(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (enable && disable) { printFailure(context, 'Cannot use both --enable and --disable'); - process.exit(1); + exitWithError('Cannot use both --enable and --disable', context); } if ( @@ -47,12 +48,15 @@ export default async function setTransitions(options: Record) { context, 'Cannot use --disable with --days, --date, or --storage-class' ); - process.exit(1); + exitWithError( + 'Cannot use --disable with --days, --date, or --storage-class', + context + ); } if (!enable && !disable && days === undefined && date === undefined) { printFailure(context, 'Provide --days, --date, --enable, or --disable'); - process.exit(1); + exitWithError('Provide --days, --date, --enable, or --disable', context); } if ((days !== undefined || date !== undefined) && !storageClass) { @@ -60,7 +64,10 @@ export default async function setTransitions(options: Record) { context, '--storage-class is required when setting --days or --date' ); - process.exit(1); + exitWithError( + '--storage-class is required when setting --days or --date', + context + ); } if (storageClass && !VALID_TRANSITION_CLASSES.includes(storageClass)) { @@ -68,12 +75,15 @@ export default async function setTransitions(options: Record) { context, `--storage-class must be one of: ${VALID_TRANSITION_CLASSES.join(', ')} (STANDARD is not a valid transition target)` ); - process.exit(1); + exitWithError( + `--storage-class must be one of: ${VALID_TRANSITION_CLASSES.join(', ')} (STANDARD is not a valid transition target)`, + context + ); } if (days !== undefined && (isNaN(Number(days)) || Number(days) <= 0)) { printFailure(context, '--days must be a positive number'); - process.exit(1); + exitWithError('--days must be a positive number', context); } if (date !== undefined) { @@ -86,7 +96,10 @@ export default async function setTransitions(options: Record) { context, '--date must be a valid ISO-8601 date (e.g. 2026-06-01)' ); - process.exit(1); + exitWithError( + '--date must be a valid ISO-8601 date (e.g. 2026-06-01)', + context + ); } } @@ -116,7 +129,7 @@ export default async function setTransitions(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set-ttl.ts b/src/lib/buckets/set-ttl.ts index 89a3ea7..a186864 100644 --- a/src/lib/buckets/set-ttl.ts +++ b/src/lib/buckets/set-ttl.ts @@ -8,6 +8,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set-ttl'); @@ -22,27 +23,27 @@ export default async function setTtl(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (enable && disable) { printFailure(context, 'Cannot use both --enable and --disable'); - process.exit(1); + exitWithError('Cannot use both --enable and --disable', context); } if (disable && (days !== undefined || date !== undefined)) { printFailure(context, 'Cannot use --disable with --days or --date'); - process.exit(1); + exitWithError('Cannot use --disable with --days or --date', context); } if (!enable && !disable && days === undefined && date === undefined) { printFailure(context, 'Provide --days, --date, --enable, or --disable'); - process.exit(1); + exitWithError('Provide --days, --date, --enable, or --disable', context); } if (days !== undefined && (isNaN(Number(days)) || Number(days) <= 0)) { printFailure(context, '--days must be a positive number'); - process.exit(1); + exitWithError('--days must be a positive number', context); } if (date !== undefined) { @@ -55,7 +56,10 @@ export default async function setTtl(options: Record) { context, '--date must be a valid ISO-8601 date (e.g. 2026-06-01)' ); - process.exit(1); + exitWithError( + '--date must be a valid ISO-8601 date (e.g. 2026-06-01)', + context + ); } } @@ -82,7 +86,7 @@ export default async function setTtl(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name }); diff --git a/src/lib/buckets/set.ts b/src/lib/buckets/set.ts index 2d70f3a..720fb9b 100644 --- a/src/lib/buckets/set.ts +++ b/src/lib/buckets/set.ts @@ -9,6 +9,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('buckets', 'set'); @@ -61,7 +62,7 @@ export default async function set(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } // Check if at least one setting is provided @@ -76,7 +77,7 @@ export default async function set(options: Record) { enableAdditionalHeaders === undefined ) { printFailure(context, 'At least one setting is required'); - process.exit(1); + exitWithError('At least one setting is required', context); } const config = await getStorageConfig(); @@ -136,7 +137,7 @@ export default async function set(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/configure/index.ts b/src/lib/configure/index.ts index 0795a6d..45ffba4 100644 --- a/src/lib/configure/index.ts +++ b/src/lib/configure/index.ts @@ -9,6 +9,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; const context = msg('configure'); @@ -78,7 +79,7 @@ export default async function configure(options: Record) { // Validate that all required fields are present if (!accessKey || !accessSecret || !endpoint) { printFailure(context, 'All credentials are required'); - process.exit(1); + exitWithError('All credentials are required', context); } // Store credentials @@ -93,8 +94,9 @@ export default async function configure(options: Record) { await storeLoginMethod('credentials'); printSuccess(context); - } catch { + printNextActions(context); + } catch (error) { printFailure(context, 'Failed to save credentials'); - process.exit(1); + exitWithError(error, context); } } diff --git a/src/lib/cp.ts b/src/lib/cp.ts index 7937657..6b69913 100644 --- a/src/lib/cp.ts +++ b/src/lib/cp.ts @@ -25,6 +25,7 @@ import { get, put, list, head } from '@tigrisdata/storage'; import { executeWithConcurrency } from '../utils/concurrency.js'; import { calculateUploadParams } from '../utils/upload.js'; import type { ParsedPath } from '../types.js'; +import { exitWithError } from '../utils/exit.js'; let _jsonMode = false; @@ -35,10 +36,9 @@ function detectDirection(src: string, dest: string): CopyDirection { const destRemote = isRemotePath(dest); if (!srcRemote && !destRemote) { - console.error( + exitWithError( 'At least one path must be a remote Tigris path (t3:// or tigris://)' ); - process.exit(1); } if (srcRemote && destRemote) return 'remote-to-remote'; @@ -342,16 +342,14 @@ async function copyLocalToRemote( try { stats = statSync(localPath); } catch { - console.error(`Source not found: ${src}`); - process.exit(1); + exitWithError(`Source not found: ${src}`); } if (stats.isDirectory()) { if (!recursive) { - console.error( + exitWithError( `${src} is a directory (not copied). Use -r to copy recursively.` ); - process.exit(1); } const files = listLocalFiles(localPath); @@ -438,8 +436,7 @@ async function copyLocalToRemote( !_jsonMode ); if (result.error) { - console.error(result.error); - process.exit(1); + exitWithError(result.error); } if (_jsonMode) { console.log( @@ -469,8 +466,7 @@ async function copyRemoteToLocal( // t3://bucket/ (no path, trailing slash) = copy all contents from bucket root const rawEndsWithSlash = src.endsWith('/'); if (!srcParsed.path && !rawEndsWithSlash) { - console.error('Cannot copy a bucket. Provide a path within the bucket.'); - process.exit(1); + exitWithError('Cannot copy a bucket. Provide a path within the bucket.'); } const localDest = resolveLocalPath(dest); @@ -483,10 +479,9 @@ async function copyRemoteToLocal( } if (isFolder && !isWildcard && !recursive) { - console.error( - `Source is a remote folder (not copied). Use -r to copy recursively.` + exitWithError( + 'Source is a remote folder (not copied). Use -r to copy recursively.' ); - process.exit(1); } if (isWildcard || isFolder) { @@ -511,8 +506,7 @@ async function copyRemoteToLocal( ); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } let filesToDownload = items.filter((item) => !item.name.endsWith('/')); @@ -605,8 +599,7 @@ async function copyRemoteToLocal( !_jsonMode ); if (result.error) { - console.error(result.error); - process.exit(1); + exitWithError(result.error); } if (_jsonMode) { console.log( @@ -638,8 +631,7 @@ async function copyRemoteToRemote( // t3://bucket/ (no path, trailing slash) = copy all contents from bucket root const rawEndsWithSlash = src.endsWith('/'); if (!srcParsed.path && !rawEndsWithSlash) { - console.error('Cannot copy a bucket. Provide a path within the bucket.'); - process.exit(1); + exitWithError('Cannot copy a bucket. Provide a path within the bucket.'); } const isWildcard = src.includes('*'); @@ -651,10 +643,9 @@ async function copyRemoteToRemote( } if (isFolder && !isWildcard && !recursive) { - console.error( - `Source is a remote folder (not copied). Use -r to copy recursively.` + exitWithError( + 'Source is a remote folder (not copied). Use -r to copy recursively.' ); - process.exit(1); } if (isWildcard || isFolder) { @@ -684,8 +675,7 @@ async function copyRemoteToRemote( srcParsed.bucket === destParsed.bucket && prefix === effectiveDestPrefixWithSlash ) { - console.error('Source and destination are the same'); - process.exit(1); + exitWithError('Source and destination are the same'); } const { items, error } = await listAllItems( @@ -695,8 +685,7 @@ async function copyRemoteToRemote( ); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } let itemsToCopy = items.filter((item) => item.name !== prefix); @@ -821,8 +810,7 @@ async function copyRemoteToRemote( } if (srcParsed.bucket === destParsed.bucket && srcParsed.path === destKey) { - console.error('Source and destination are the same'); - process.exit(1); + exitWithError('Source and destination are the same'); } const result = await copyObject( @@ -835,8 +823,7 @@ async function copyRemoteToRemote( ); if (result.error) { - console.error(result.error); - process.exit(1); + exitWithError(result.error); } if (_jsonMode) { @@ -862,8 +849,7 @@ export default async function cp(options: Record) { const dest = getOption(options, ['dest']); if (!src || !dest) { - console.error('Both src and dest arguments are required'); - process.exit(1); + exitWithError('Both src and dest arguments are required'); } const recursive = !!getOption(options, ['recursive', 'r']); @@ -880,8 +866,7 @@ export default async function cp(options: Record) { case 'local-to-remote': { const destParsed = parseRemotePath(dest); if (!destParsed.bucket) { - console.error('Invalid destination path'); - process.exit(1); + exitWithError('Invalid destination path'); } await copyLocalToRemote(src, destParsed, config, recursive); break; @@ -889,8 +874,7 @@ export default async function cp(options: Record) { case 'remote-to-local': { const srcParsed = parseRemotePath(src); if (!srcParsed.bucket) { - console.error('Invalid source path'); - process.exit(1); + exitWithError('Invalid source path'); } await copyRemoteToLocal(src, srcParsed, dest, config, recursive); break; @@ -899,12 +883,10 @@ export default async function cp(options: Record) { const srcParsed = parseRemotePath(src); const destParsed = parseRemotePath(dest); if (!srcParsed.bucket) { - console.error('Invalid source path'); - process.exit(1); + exitWithError('Invalid source path'); } if (!destParsed.bucket) { - console.error('Invalid destination path'); - process.exit(1); + exitWithError('Invalid destination path'); } await copyRemoteToRemote(src, srcParsed, destParsed, config, recursive); break; diff --git a/src/lib/credentials/test.ts b/src/lib/credentials/test.ts index 99443d2..2db94ef 100644 --- a/src/lib/credentials/test.ts +++ b/src/lib/credentials/test.ts @@ -8,6 +8,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('credentials', 'test'); @@ -28,7 +29,10 @@ export default async function test(options: Record) { context, 'No credentials found. Run "tigris configure" or "tigris login" first.' ); - process.exit(1); + exitWithError( + 'No credentials found. Run "tigris configure" or "tigris login" first.', + context + ); } // Include organization ID if available @@ -51,7 +55,7 @@ export default async function test(options: Record) { context, `Current credentials don't have access to bucket "${bucket}"` ); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { @@ -75,7 +79,7 @@ export default async function test(options: Record) { if (error) { printFailure(context, "Current credentials don't have sufficient access"); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/forks/create.ts b/src/lib/forks/create.ts index d15528c..b8b5d76 100644 --- a/src/lib/forks/create.ts +++ b/src/lib/forks/create.ts @@ -7,6 +7,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('forks', 'create'); @@ -19,12 +20,12 @@ export default async function create(options: Record) { if (!name) { printFailure(context, 'Source bucket name is required'); - process.exit(1); + exitWithError('Source bucket name is required', context); } if (!forkName) { printFailure(context, 'Fork name is required'); - process.exit(1); + exitWithError('Fork name is required', context); } const { error } = await createBucket(forkName, { @@ -35,7 +36,7 @@ export default async function create(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name, forkName }); diff --git a/src/lib/forks/list.ts b/src/lib/forks/list.ts index 9d13c4d..05d9e1d 100644 --- a/src/lib/forks/list.ts +++ b/src/lib/forks/list.ts @@ -9,6 +9,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('forks', 'list'); @@ -23,7 +24,7 @@ export default async function list(options: Record) { if (!name) { printFailure(context, 'Source bucket name is required'); - process.exit(1); + exitWithError('Source bucket name is required', context); } const config = await getStorageConfig(); @@ -35,7 +36,7 @@ export default async function list(options: Record) { if (infoError) { printFailure(context, infoError.message); - process.exit(1); + exitWithError(infoError, context); } if (!bucketInfo.hasForks) { @@ -48,7 +49,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } // Get info for each bucket to find forks diff --git a/src/lib/iam/policies/create.ts b/src/lib/iam/policies/create.ts index f6df64a..606a484 100644 --- a/src/lib/iam/policies/create.ts +++ b/src/lib/iam/policies/create.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; import { readStdin, parseDocument } from './utils.js'; const context = msg('iam policies', 'create'); @@ -24,7 +25,7 @@ export default async function create(options: Record) { if (!name) { printFailure(context, 'Policy name is required'); - process.exit(1); + exitWithError('Policy name is required', context); } // Validate policy name: only alphanumeric and =,.@_- allowed @@ -34,7 +35,10 @@ export default async function create(options: Record) { context, 'Invalid policy name. Only alphanumeric characters and =,.@_- are allowed.' ); - process.exit(1); + exitWithError( + 'Invalid policy name. Only alphanumeric characters and =,.@_- are allowed.', + context + ); } const loginMethod = await getLoginMethod(); @@ -44,7 +48,10 @@ export default async function create(options: Record) { context, 'Policies can only be created when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Policies can only be created when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -52,7 +59,10 @@ export default async function create(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -84,7 +94,10 @@ export default async function create(options: Record) { context, 'Policy document is required. Provide via --document or pipe to stdin.' ); - process.exit(1); + exitWithError( + 'Policy document is required. Provide via --document or pipe to stdin.', + context + ); } // Parse and convert document @@ -93,7 +106,7 @@ export default async function create(options: Record) { document = parseDocument(documentJson); } catch { printFailure(context, 'Invalid JSON in policy document'); - process.exit(1); + exitWithError('Invalid JSON in policy document', context); } const { data, error } = await addPolicy(name, { @@ -104,7 +117,7 @@ export default async function create(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { name: data.name }); diff --git a/src/lib/iam/policies/delete.ts b/src/lib/iam/policies/delete.ts index acfa464..806462d 100644 --- a/src/lib/iam/policies/delete.ts +++ b/src/lib/iam/policies/delete.ts @@ -14,6 +14,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam policies', 'delete'); @@ -30,7 +31,10 @@ export default async function del(options: Record) { context, 'Policies can only be deleted when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Policies can only be deleted when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -38,7 +42,10 @@ export default async function del(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -59,7 +66,7 @@ export default async function del(options: Record) { if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (!listData.policies || listData.policies.length === 0) { @@ -97,7 +104,7 @@ export default async function del(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { resource }); diff --git a/src/lib/iam/policies/edit.ts b/src/lib/iam/policies/edit.ts index 3cb7cd4..c39a6b8 100644 --- a/src/lib/iam/policies/edit.ts +++ b/src/lib/iam/policies/edit.ts @@ -20,6 +20,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; import { readStdin, parseDocument } from './utils.js'; const context = msg('iam policies', 'edit'); @@ -38,7 +39,10 @@ export default async function edit(options: Record) { context, 'Policies can only be edited when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Policies can only be edited when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -46,7 +50,10 @@ export default async function edit(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -67,7 +74,10 @@ export default async function edit(options: Record) { context, 'Policy ARN is required when piping document via stdin.' ); - process.exit(1); + exitWithError( + 'Policy ARN is required when piping document via stdin.', + context + ); } const { data: listData, error: listError } = await listPolicies({ @@ -76,7 +86,7 @@ export default async function edit(options: Record) { if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (!listData.policies || listData.policies.length === 0) { @@ -115,7 +125,7 @@ export default async function edit(options: Record) { newDocument = parseDocument(documentJson); } catch { printFailure(context, 'Invalid JSON in policy document'); - process.exit(1); + exitWithError('Invalid JSON in policy document', context); } } else if (!process.stdin.isTTY && !description) { // Read from stdin only if no description provided (description-only update doesn't need stdin) @@ -124,13 +134,13 @@ export default async function edit(options: Record) { newDocument = parseDocument(documentJson); } catch { printFailure(context, 'Invalid JSON in policy document'); - process.exit(1); + exitWithError('Invalid JSON in policy document', context); } } if (!newDocument && !description) { printFailure(context, 'Either --document or --description is required.'); - process.exit(1); + exitWithError('Either --document or --description is required.', context); } // Fetch existing policy to fill in missing values @@ -140,7 +150,7 @@ export default async function edit(options: Record) { if (getError) { printFailure(context, getError.message); - process.exit(1); + exitWithError(getError, context); } const { data, error } = await editPolicy(resource, { @@ -151,7 +161,7 @@ export default async function edit(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { resource: data.resource }); diff --git a/src/lib/iam/policies/get.ts b/src/lib/iam/policies/get.ts index c731ff0..6f67977 100644 --- a/src/lib/iam/policies/get.ts +++ b/src/lib/iam/policies/get.ts @@ -14,6 +14,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam policies', 'get'); @@ -33,7 +34,10 @@ export default async function get(options: Record) { context, 'Policies can only be retrieved when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Policies can only be retrieved when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -41,7 +45,10 @@ export default async function get(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -62,7 +69,7 @@ export default async function get(options: Record) { if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (!listData.policies || listData.policies.length === 0) { @@ -89,7 +96,7 @@ export default async function get(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/iam/policies/list.ts b/src/lib/iam/policies/list.ts index aa1cf6a..90cae64 100644 --- a/src/lib/iam/policies/list.ts +++ b/src/lib/iam/policies/list.ts @@ -12,6 +12,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam policies', 'list'); @@ -30,7 +31,10 @@ export default async function list(options: Record) { context, 'Policies can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Policies can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const authClient = getAuthClient(); @@ -38,7 +42,10 @@ export default async function list(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -55,7 +62,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data.policies || data.policies.length === 0) { diff --git a/src/lib/iam/users/invite.ts b/src/lib/iam/users/invite.ts index 94c54ef..539f7cb 100644 --- a/src/lib/iam/users/invite.ts +++ b/src/lib/iam/users/invite.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam users', 'invite'); @@ -24,7 +25,10 @@ export default async function invite(options: Record) { context, 'Users can only be invited when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Users can only be invited when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const selectedOrg = getSelectedOrganization(); @@ -49,7 +53,7 @@ export default async function invite(options: Record) { if (emails.length === 0) { printFailure(context, 'Email address is required'); - process.exit(1); + exitWithError('Email address is required', context); } const validRoles = ['admin', 'member'] as const; @@ -60,7 +64,10 @@ export default async function invite(options: Record) { context, `Invalid role "${roleInput}". Must be one of: ${validRoles.join(', ')}` ); - process.exit(1); + exitWithError( + `Invalid role "${roleInput}". Must be one of: ${validRoles.join(', ')}`, + context + ); } const role: Role = roleInput as Role; @@ -70,7 +77,10 @@ export default async function invite(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -89,7 +99,7 @@ export default async function invite(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { email: emails.join(', ') }); diff --git a/src/lib/iam/users/list.ts b/src/lib/iam/users/list.ts index 81a35cc..71800fe 100644 --- a/src/lib/iam/users/list.ts +++ b/src/lib/iam/users/list.ts @@ -18,6 +18,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam users', 'list'); @@ -36,7 +37,10 @@ export default async function list(options: Record) { context, 'Users can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Users can only be listed when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const selectedOrg = getSelectedOrganization(); @@ -55,7 +59,10 @@ export default async function list(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -72,7 +79,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const users = data.users.map((user) => ({ diff --git a/src/lib/iam/users/remove.ts b/src/lib/iam/users/remove.ts index 6e1cac3..a000b62 100644 --- a/src/lib/iam/users/remove.ts +++ b/src/lib/iam/users/remove.ts @@ -15,6 +15,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam users', 'remove'); @@ -31,7 +32,10 @@ export default async function removeUser(options: Record) { context, 'Users can only be removed when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Users can only be removed when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const selectedOrg = getSelectedOrganization(); @@ -50,7 +54,10 @@ export default async function removeUser(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -77,7 +84,7 @@ export default async function removeUser(options: Record) { if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (listData.users.length === 0) { @@ -115,7 +122,7 @@ export default async function removeUser(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context); diff --git a/src/lib/iam/users/revoke-invitation.ts b/src/lib/iam/users/revoke-invitation.ts index cabd1ea..03def7b 100644 --- a/src/lib/iam/users/revoke-invitation.ts +++ b/src/lib/iam/users/revoke-invitation.ts @@ -15,6 +15,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam users', 'revoke-invitation'); @@ -33,7 +34,10 @@ export default async function revokeInvitation( context, 'Invitations can only be revoked when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'Invitations can only be revoked when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const selectedOrg = getSelectedOrganization(); @@ -52,7 +56,10 @@ export default async function revokeInvitation( if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -79,7 +86,7 @@ export default async function revokeInvitation( if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (listData.invitations.length === 0) { @@ -120,7 +127,7 @@ export default async function revokeInvitation( if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context); diff --git a/src/lib/iam/users/update-role.ts b/src/lib/iam/users/update-role.ts index 52e6ef6..8c0a54a 100644 --- a/src/lib/iam/users/update-role.ts +++ b/src/lib/iam/users/update-role.ts @@ -15,6 +15,7 @@ import { printEmpty, msg, } from '../../../utils/messages.js'; +import { exitWithError } from '../../../utils/exit.js'; const context = msg('iam users', 'update-role'); @@ -28,7 +29,10 @@ export default async function updateRole(options: Record) { context, 'User roles can only be updated when logged in via OAuth.\nRun "tigris login oauth" first.' ); - process.exit(1); + exitWithError( + 'User roles can only be updated when logged in via OAuth.\nRun "tigris login oauth" first.', + context + ); } const selectedOrg = getSelectedOrganization(); @@ -52,7 +56,10 @@ export default async function updateRole(options: Record) { context, 'Role is required. Use --role admin or --role member' ); - process.exit(1); + exitWithError( + 'Role is required. Use --role admin or --role member', + context + ); } const roles = Array.isArray(roleOption) ? roleOption : [roleOption]; @@ -63,7 +70,10 @@ export default async function updateRole(options: Record) { context, `Invalid role "${r}". Must be one of: ${validRoles.join(', ')}` ); - process.exit(1); + exitWithError( + `Invalid role "${r}". Must be one of: ${validRoles.join(', ')}`, + context + ); } } @@ -80,7 +90,10 @@ export default async function updateRole(options: Record) { if (!isAuthenticated) { printFailure(context, 'Not authenticated. Run "tigris login oauth" first.'); - process.exit(1); + exitWithError( + 'Not authenticated. Run "tigris login oauth" first.', + context + ); } const accessToken = await authClient.getAccessToken(); @@ -101,7 +114,7 @@ export default async function updateRole(options: Record) { if (listError) { printFailure(context, listError.message); - process.exit(1); + exitWithError(listError, context); } if (listData.users.length === 0) { @@ -130,7 +143,10 @@ export default async function updateRole(options: Record) { context, `Number of roles (${roles.length}) must match number of users (${resources.length}), or provide a single role for all users` ); - process.exit(1); + exitWithError( + `Number of roles (${roles.length}) must match number of users (${resources.length}), or provide a single role for all users`, + context + ); } const roleUpdates = resources.map((userId, i) => ({ @@ -144,7 +160,7 @@ export default async function updateRole(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context); diff --git a/src/lib/login/credentials.ts b/src/lib/login/credentials.ts index 35b5800..8472d1d 100644 --- a/src/lib/login/credentials.ts +++ b/src/lib/login/credentials.ts @@ -13,6 +13,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; const context = msg('login', 'credentials'); @@ -73,7 +74,7 @@ export default async function credentials(options: Record) { // Validate if (!accessKey || !accessSecret) { printFailure(context, 'Access key and secret are required'); - process.exit(1); + exitWithError('Access key and secret are required', context); } // Get endpoint: configured → default @@ -89,4 +90,5 @@ export default async function credentials(options: Record) { await storeLoginMethod('credentials'); printSuccess(context); + printNextActions(context); } diff --git a/src/lib/login/oauth.ts b/src/lib/login/oauth.ts index 9f90a02..edb1f96 100644 --- a/src/lib/login/oauth.ts +++ b/src/lib/login/oauth.ts @@ -8,6 +8,7 @@ import { printHint, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; const context = msg('login', 'oauth'); @@ -38,16 +39,18 @@ export async function oauth(): Promise { const firstOrg = orgs[0]; await storeSelectedOrganization(firstOrg.id); printSuccess(context, { org: firstOrg.displayName || firstOrg.name }); + printNextActions(context); if (orgs.length > 1) { printHint(context, { count: orgs.length }); } } else { printSuccess(context, { org: 'none' }); + printNextActions(context); } - } catch { + } catch (error) { printFailure(context); - process.exit(1); + exitWithError(error, context); } } diff --git a/src/lib/logout.ts b/src/lib/logout.ts index 6e83527..b26e4d7 100644 --- a/src/lib/logout.ts +++ b/src/lib/logout.ts @@ -5,6 +5,7 @@ import { printFailure, msg, } from '../utils/messages.js'; +import { exitWithError } from '../utils/exit.js'; const context = msg('logout'); @@ -21,6 +22,6 @@ export default async function logout(): Promise { } else { printFailure(context); } - process.exit(1); + exitWithError(error, context); } } diff --git a/src/lib/ls.ts b/src/lib/ls.ts index eed18d7..ae5ddcc 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -3,6 +3,7 @@ import { getOption } from '../utils/options.js'; import { formatOutput, formatSize } from '../utils/format.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { list, listBuckets } from '@tigrisdata/storage'; +import { exitWithError } from '../utils/exit.js'; export default async function ls(options: Record) { const pathString = getOption(options, ['path']); @@ -22,8 +23,7 @@ export default async function ls(options: Record) { const { data, error } = await listBuckets({ config }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } const buckets = (data.buckets || []).map((bucket) => ({ @@ -43,8 +43,7 @@ export default async function ls(options: Record) { const { bucket, path } = parseAnyPath(pathString); if (!bucket) { - console.error('Invalid path'); - process.exit(1); + exitWithError('Invalid path'); } const config = await getStorageConfig(); @@ -62,8 +61,7 @@ export default async function ls(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } const objects = (data.items || []) diff --git a/src/lib/mk.ts b/src/lib/mk.ts index cf6024e..6fe0200 100644 --- a/src/lib/mk.ts +++ b/src/lib/mk.ts @@ -3,20 +3,19 @@ import { getOption } from '../utils/options.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { createBucket, put, type StorageClass } from '@tigrisdata/storage'; import { parseLocations } from '../utils/locations.js'; +import { exitWithError } from '../utils/exit.js'; export default async function mk(options: Record) { const pathString = getOption(options, ['path']); if (!pathString) { - console.error('path argument is required'); - process.exit(1); + exitWithError('path argument is required'); } const { bucket, path } = parseAnyPath(pathString); if (!bucket) { - console.error('Invalid path'); - process.exit(1); + exitWithError('Invalid path'); } const config = await getStorageConfig(); @@ -73,8 +72,7 @@ export default async function mk(options: Record) { } if (sourceSnapshot && !forkOf) { - console.error('--source-snapshot requires --fork-of'); - process.exit(1); + exitWithError('--source-snapshot requires --fork-of'); } const { error } = await createBucket(bucket, { @@ -88,8 +86,7 @@ export default async function mk(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (format === 'json') { @@ -112,8 +109,7 @@ export default async function mk(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (format === 'json') { diff --git a/src/lib/mv.ts b/src/lib/mv.ts index c2b68c4..e7e5d90 100644 --- a/src/lib/mv.ts +++ b/src/lib/mv.ts @@ -12,6 +12,7 @@ import { formatSize } from '../utils/format.js'; import { requireInteractive, confirm } from '../utils/interactive.js'; import { get, put, remove, list, head } from '@tigrisdata/storage'; import { calculateUploadParams } from '../utils/upload.js'; +import { exitWithError } from '../utils/exit.js'; let _jsonMode = false; @@ -27,28 +28,24 @@ export default async function mv(options: Record) { _jsonMode = format === 'json'; if (!src || !dest) { - console.error('both src and dest arguments are required'); - process.exit(1); + exitWithError('both src and dest arguments are required'); } if (!isRemotePath(src) || !isRemotePath(dest)) { - console.error( + exitWithError( 'Both src and dest must be remote Tigris paths (t3:// or tigris://)' ); - process.exit(1); } const srcPath = parseRemotePath(src); const destPath = parseRemotePath(dest); if (!srcPath.bucket) { - console.error('Invalid source path'); - process.exit(1); + exitWithError('Invalid source path'); } if (!destPath.bucket) { - console.error('Invalid destination path'); - process.exit(1); + exitWithError('Invalid destination path'); } // Cannot move a bucket itself @@ -56,8 +53,7 @@ export default async function mv(options: Record) { // t3://bucket/ (no path, trailing slash) = move all contents from bucket root const rawEndsWithSlash = src.endsWith('/'); if (!srcPath.path && !rawEndsWithSlash) { - console.error('Cannot move a bucket. Provide a path within the bucket.'); - process.exit(1); + exitWithError('Cannot move a bucket. Provide a path within the bucket.'); } const config = await getStorageConfig({ withCredentialProvider: true }); @@ -73,10 +69,9 @@ export default async function mv(options: Record) { } if (isFolder && !isWildcard && !recursive) { - console.error( - `Source is a remote folder (not moved). Use -r to move recursively.` + exitWithError( + 'Source is a remote folder (not moved). Use -r to move recursively.' ); - process.exit(1); } if (isWildcard || isFolder) { @@ -107,8 +102,7 @@ export default async function mv(options: Record) { srcPath.bucket === destPath.bucket && prefix === effectiveDestPrefixWithSlash ) { - console.error('Source and destination are the same'); - process.exit(1); + exitWithError('Source and destination are the same'); } const { items, error } = await listAllItems( @@ -118,8 +112,7 @@ export default async function mv(options: Record) { ); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } // Filter out folder markers - they're handled separately below @@ -268,8 +261,7 @@ export default async function mv(options: Record) { // Check for same location if (srcPath.bucket === destPath.bucket && srcPath.path === destKey) { - console.error('Source and destination are the same'); - process.exit(1); + exitWithError('Source and destination are the same'); } if (!force) { @@ -293,8 +285,7 @@ export default async function mv(options: Record) { ); if (result.error) { - console.error(result.error); - process.exit(1); + exitWithError(result.error); } if (_jsonMode) { diff --git a/src/lib/objects/delete.ts b/src/lib/objects/delete.ts index 4cec92f..53e0b14 100644 --- a/src/lib/objects/delete.ts +++ b/src/lib/objects/delete.ts @@ -7,6 +7,11 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../utils/exit.js'; import { requireInteractive, confirm } from '../../utils/interactive.js'; const context = msg('objects', 'delete'); @@ -25,12 +30,12 @@ export default async function deleteObject(options: Record) { if (!bucket) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (!keys) { printFailure(context, 'Object key is required'); - process.exit(1); + exitWithError('Object key is required', context); } const config = await getStorageConfig(); @@ -67,12 +72,20 @@ export default async function deleteObject(options: Record) { } if (format === 'json') { - console.log( - JSON.stringify({ action: 'deleted', bucket, keys: deleted, errors }) - ); + const nextActions = getSuccessNextActions(context, { bucket }); + const jsonOutput: Record = { + action: 'deleted', + bucket, + keys: deleted, + errors, + }; + if (nextActions.length > 0) jsonOutput.nextActions = nextActions; + console.log(JSON.stringify(jsonOutput)); } if (errors.length > 0) { - process.exit(1); + exitWithError(errors[0].error, context); } + + printNextActions(context, { bucket }); } diff --git a/src/lib/objects/get.ts b/src/lib/objects/get.ts index 16560e0..1f06fab 100644 --- a/src/lib/objects/get.ts +++ b/src/lib/objects/get.ts @@ -11,6 +11,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('objects', 'get'); @@ -125,12 +126,12 @@ export default async function getObject(options: Record) { if (!bucket) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (!key) { printFailure(context, 'Object key is required'); - process.exit(1); + exitWithError('Object key is required', context); } const config = await getStorageConfig(); @@ -149,7 +150,7 @@ export default async function getObject(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (output) { @@ -177,7 +178,7 @@ export default async function getObject(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (output) { diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index 16dfdf6..703d574 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -9,6 +9,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('objects', 'list'); @@ -29,7 +30,7 @@ export default async function listObjects(options: Record) { if (!bucket) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const config = await getStorageConfig(); @@ -45,7 +46,7 @@ export default async function listObjects(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data.items || data.items.length === 0) { diff --git a/src/lib/objects/put.ts b/src/lib/objects/put.ts index d4c7df1..8c6d2e2 100644 --- a/src/lib/objects/put.ts +++ b/src/lib/objects/put.ts @@ -10,6 +10,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; import { calculateUploadParams } from '../../utils/upload.js'; const context = msg('objects', 'put'); @@ -34,12 +35,12 @@ export default async function putObject(options: Record) { if (!bucket) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (!key) { printFailure(context, 'Object key is required'); - process.exit(1); + exitWithError('Object key is required', context); } // Check for stdin or file input @@ -47,7 +48,7 @@ export default async function putObject(options: Record) { if (!file && !hasStdin) { printFailure(context, 'File path is required (or pipe data via stdin)'); - process.exit(1); + exitWithError('File path is required (or pipe data via stdin)', context); } let body: ReadableStream; @@ -60,7 +61,7 @@ export default async function putObject(options: Record) { fileSize = stats.size; } catch { printFailure(context, `File not found: ${file}`); - process.exit(1); + exitWithError(`File not found: ${file}`, context); } const fileStream = createReadStream(file); body = Readable.toWeb(fileStream) as ReadableStream; @@ -100,7 +101,7 @@ export default async function putObject(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const result = [ @@ -121,4 +122,5 @@ export default async function putObject(options: Record) { console.log(output); printSuccess(context, { key, bucket }); + printNextActions(context, { key, bucket }); } diff --git a/src/lib/objects/set.ts b/src/lib/objects/set.ts index d89926d..6eb096c 100644 --- a/src/lib/objects/set.ts +++ b/src/lib/objects/set.ts @@ -6,6 +6,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; import { updateObject } from '@tigrisdata/storage'; const context = msg('objects', 'set'); @@ -25,17 +26,20 @@ export default async function setObject(options: Record) { if (!bucket) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } if (!key) { printFailure(context, 'Object key is required'); - process.exit(1); + exitWithError('Object key is required', context); } if (!access) { printFailure(context, 'Access level is required (--access public|private)'); - process.exit(1); + exitWithError( + 'Access level is required (--access public|private)', + context + ); } const config = await getStorageConfig(); @@ -51,7 +55,7 @@ export default async function setObject(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (format === 'json') { diff --git a/src/lib/organizations/create.ts b/src/lib/organizations/create.ts index a2777a4..2b4f4a4 100644 --- a/src/lib/organizations/create.ts +++ b/src/lib/organizations/create.ts @@ -14,6 +14,7 @@ import { printHint, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; const context = msg('organizations', 'create'); @@ -53,7 +54,7 @@ export default async function create(options: Record) { if (!name) { printFailure(context, 'Organization name is required'); - process.exit(1); + exitWithError('Organization name is required', context); } const config = await getStorageConfig(); @@ -62,11 +63,12 @@ export default async function create(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const id = data.id; printSuccess(context, { name, id }); printHint(context, { name }); + printNextActions(context, { name }); } diff --git a/src/lib/organizations/list.ts b/src/lib/organizations/list.ts index 3c9a155..0a7b912 100644 --- a/src/lib/organizations/list.ts +++ b/src/lib/organizations/list.ts @@ -19,6 +19,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('organizations', 'list'); @@ -68,7 +69,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } orgs = data?.organizations ?? []; diff --git a/src/lib/organizations/select.ts b/src/lib/organizations/select.ts index cc7180e..8696bcf 100644 --- a/src/lib/organizations/select.ts +++ b/src/lib/organizations/select.ts @@ -12,6 +12,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError, printNextActions } from '../../utils/exit.js'; const context = msg('organizations', 'select'); @@ -40,7 +41,7 @@ export default async function select(options: Record) { if (!name) { printFailure(context, 'Organization name or ID is required'); - process.exit(1); + exitWithError('Organization name or ID is required', context); } const config = await getStorageConfig(); @@ -49,7 +50,7 @@ export default async function select(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const orgs = data?.organizations ?? []; @@ -65,11 +66,12 @@ export default async function select(options: Record) { context, `Organization "${name}" not found\n\nAvailable organizations:\n${availableOrgs}` ); - process.exit(1); + exitWithError(`Organization "${name}" not found`, context); } // Store selected organization await storeSelectedOrganization(org.id); printSuccess(context, { name: org.name }); + printNextActions(context, { name: org.name }); } diff --git a/src/lib/presign.ts b/src/lib/presign.ts index 4af628b..ec33c5c 100644 --- a/src/lib/presign.ts +++ b/src/lib/presign.ts @@ -8,6 +8,7 @@ import { getAuthClient } from '../auth/client.js'; import { getSelectedOrganization } from '../auth/storage.js'; import { getTigrisConfig } from '../auth/config.js'; import { formatJson } from '../utils/format.js'; +import { exitWithError } from '../utils/exit.js'; import enquirer from 'enquirer'; const { prompt } = enquirer; @@ -15,20 +16,17 @@ export default async function presign(options: Record) { const pathString = getOption(options, ['path']); if (!pathString) { - console.error('path argument is required'); - process.exit(1); + exitWithError('path argument is required'); } const { bucket, path } = parseAnyPath(pathString); if (!bucket) { - console.error('Invalid path'); - process.exit(1); + exitWithError('Invalid path'); } if (!path) { - console.error('Object key is required'); - process.exit(1); + exitWithError('Object key is required'); } const method = getOption(options, ['method', 'm']) ?? 'get'; @@ -58,20 +56,18 @@ export default async function presign(options: Record) { const loginMethod = await getLoginMethod(); if (loginMethod !== 'oauth') { - console.error( + exitWithError( 'Presigning requires an access key. Pass --access-key or configure credentials.' ); - process.exit(1); } accessKeyId = await resolveAccessKeyInteractively(bucket); } if (!accessKeyId) { - console.error( + exitWithError( 'Presigning requires an access key. Pass --access-key or configure credentials.' ); - process.exit(1); } const { data, error } = await getPresignedUrl(path, { @@ -85,8 +81,7 @@ export default async function presign(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (format === 'json') { @@ -110,10 +105,9 @@ async function resolveAccessKeyInteractively( targetBucket: string ): Promise { if (!process.stdin.isTTY) { - console.error( + exitWithError( 'Presigning requires an access key. Pass --access-key tid_...' ); - process.exit(1); } const authClient = getAuthClient(); @@ -130,15 +124,13 @@ async function resolveAccessKeyInteractively( }); if (error) { - console.error(`Failed to list access keys: ${error.message}`); - process.exit(1); + exitWithError(error); } if (!data.accessKeys || data.accessKeys.length === 0) { - console.error( + exitWithError( 'No access keys found. Create one with "tigris access-keys create "' ); - process.exit(1); } // Filter to active keys that have access to the target bucket @@ -159,10 +151,9 @@ async function resolveAccessKeyInteractively( ); if (activeKeys.length === 0) { - console.error( + exitWithError( 'No active access keys found. Create one with "tigris access-keys create "' ); - process.exit(1); } console.error( diff --git a/src/lib/rm.ts b/src/lib/rm.ts index 937af3c..7a14057 100644 --- a/src/lib/rm.ts +++ b/src/lib/rm.ts @@ -10,6 +10,7 @@ import { getOption } from '../utils/options.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { remove, removeBucket, list } from '@tigrisdata/storage'; import { requireInteractive, confirm } from '../utils/interactive.js'; +import { exitWithError } from '../utils/exit.js'; let _jsonMode = false; @@ -24,20 +25,17 @@ export default async function rm(options: Record) { _jsonMode = format === 'json'; if (!pathString) { - console.error('path argument is required'); - process.exit(1); + exitWithError('path argument is required'); } if (!isRemotePath(pathString)) { - console.error('Path must be a remote Tigris path (t3:// or tigris://)'); - process.exit(1); + exitWithError('Path must be a remote Tigris path (t3:// or tigris://)'); } const { bucket, path } = parseRemotePath(pathString); if (!bucket) { - console.error('Invalid path'); - process.exit(1); + exitWithError('Invalid path'); } const config = await getStorageConfig(); @@ -59,8 +57,7 @@ export default async function rm(options: Record) { const { error } = await removeBucket(bucket, { config }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (_jsonMode) { @@ -81,10 +78,9 @@ export default async function rm(options: Record) { } if (isFolder && !isWildcard && !recursive) { - console.error( - `Source is a remote folder (not removed). Use -r to remove recursively.` + exitWithError( + 'Source is a remote folder (not removed). Use -r to remove recursively.' ); - process.exit(1); } if (isWildcard || isFolder) { @@ -104,8 +100,7 @@ export default async function rm(options: Record) { ); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } let itemsToRemove = items; @@ -227,8 +222,7 @@ export default async function rm(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (_jsonMode) { diff --git a/src/lib/snapshots/list.ts b/src/lib/snapshots/list.ts index 0da6c44..8920e4b 100644 --- a/src/lib/snapshots/list.ts +++ b/src/lib/snapshots/list.ts @@ -9,6 +9,7 @@ import { printEmpty, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('snapshots', 'list'); @@ -23,7 +24,7 @@ export default async function list(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const config = await getStorageConfig(); @@ -32,7 +33,7 @@ export default async function list(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data || data.length === 0) { diff --git a/src/lib/snapshots/take.ts b/src/lib/snapshots/take.ts index 21fa60b..fd9638f 100644 --- a/src/lib/snapshots/take.ts +++ b/src/lib/snapshots/take.ts @@ -7,6 +7,7 @@ import { printFailure, msg, } from '../../utils/messages.js'; +import { exitWithError } from '../../utils/exit.js'; const context = msg('snapshots', 'take'); @@ -21,7 +22,7 @@ export default async function take(options: Record) { if (!name) { printFailure(context, 'Bucket name is required'); - process.exit(1); + exitWithError('Bucket name is required', context); } const config = await getStorageConfig(); @@ -33,7 +34,7 @@ export default async function take(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } printSuccess(context, { diff --git a/src/lib/stat.ts b/src/lib/stat.ts index b201e7e..a112044 100644 --- a/src/lib/stat.ts +++ b/src/lib/stat.ts @@ -10,6 +10,7 @@ import { msg, } from '../utils/messages.js'; import { buildBucketInfo } from '../utils/bucket-info.js'; +import { exitWithError } from '../utils/exit.js'; const context = msg('stat'); @@ -34,7 +35,7 @@ export default async function stat(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const stats = [ @@ -64,7 +65,7 @@ export default async function stat(options: Record) { if (!bucket) { printFailure(context, 'Invalid path'); - process.exit(1); + exitWithError('Invalid path', context); } // Bucket only (no path or just trailing slash): show bucket info @@ -73,7 +74,7 @@ export default async function stat(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } const info = buildBucketInfo(data).map(({ label, value }) => ({ @@ -102,12 +103,12 @@ export default async function stat(options: Record) { if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } if (!data) { printFailure(context, 'Object not found'); - process.exit(1); + exitWithError('Object not found', context); } const info = [ diff --git a/src/lib/touch.ts b/src/lib/touch.ts index 0c98914..d3c3353 100644 --- a/src/lib/touch.ts +++ b/src/lib/touch.ts @@ -2,25 +2,23 @@ import { parseAnyPath } from '../utils/path.js'; import { getOption } from '../utils/options.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { put } from '@tigrisdata/storage'; +import { exitWithError } from '../utils/exit.js'; export default async function touch(options: Record) { const pathString = getOption(options, ['path']); if (!pathString) { - console.error('path argument is required'); - process.exit(1); + exitWithError('path argument is required'); } const { bucket, path } = parseAnyPath(pathString); if (!bucket) { - console.error('Invalid path'); - process.exit(1); + exitWithError('Invalid path'); } if (!path) { - console.error('Object key is required (use mk to create buckets)'); - process.exit(1); + exitWithError('Object key is required (use mk to create buckets)'); } const json = getOption(options, ['json']); @@ -38,8 +36,7 @@ export default async function touch(options: Record) { }); if (error) { - console.error(error.message); - process.exit(1); + exitWithError(error); } if (format === 'json') { diff --git a/src/lib/whoami.ts b/src/lib/whoami.ts index 8929889..d368f93 100644 --- a/src/lib/whoami.ts +++ b/src/lib/whoami.ts @@ -7,6 +7,7 @@ import { } from '../auth/storage.js'; import { getStorageConfig } from '../auth/s3-client.js'; import { printFailure, printAlreadyDone, msg } from '../utils/messages.js'; +import { exitWithError } from '../utils/exit.js'; import { getOption } from '../utils/options.js'; const context = msg('whoami'); @@ -64,7 +65,7 @@ export default async function whoami( if (error) { printFailure(context, error.message); - process.exit(1); + exitWithError(error, context); } organizations = data?.organizations ?? []; @@ -121,6 +122,6 @@ export default async function whoami( } else { printFailure(context); } - process.exit(1); + exitWithError(error, context); } } diff --git a/src/specs.yaml b/src/specs.yaml index 5de5628..8eee053 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -101,6 +101,11 @@ commands: onStart: 'Saving credentials...' onSuccess: 'Credentials saved to ~/.tigris/config.json. You can now use all tigris commands.' onFailure: 'Failed to save credentials' + nextActions: + - command: 'tigris credentials test' + description: 'Verify your credentials are working' + - command: 'tigris ls' + description: 'List your buckets' arguments: - name: access-key description: Your Tigris access key ID @@ -156,6 +161,11 @@ commands: onFailure: 'Authentication failed' onAlreadyDone: "Already logged in.\nRun \"tigris logout\" first to switch accounts." hint: "You have {{count}} organizations.\nRun \"tigris orgs list\" to switch." + nextActions: + - command: 'tigris orgs list' + description: 'List and switch organizations' + - command: 'tigris ls' + description: 'List your buckets' # credentials - name: credentials description: Login with an access key and secret. Creates a temporary session that is cleared on logout @@ -167,6 +177,9 @@ commands: onStart: 'Authenticating...' onSuccess: 'Logged in with credentials' onFailure: 'Authentication failed' + nextActions: + - command: 'tigris ls' + description: 'List your buckets' arguments: - name: access-key description: Your access key ID (will prompt if not provided) @@ -615,6 +628,9 @@ commands: onSuccess: "Organization '{{name}}' created successfully\nOrganization ID: {{id}}" onFailure: 'Failed to create organization' hint: "Next steps:\n - Select this organization: tigris orgs select {{name}}" + nextActions: + - command: 'tigris orgs select {{name}}' + description: 'Select this organization as active' arguments: - name: name type: positional @@ -631,6 +647,11 @@ commands: onStart: '' onSuccess: "Organization '{{name}}' selected" onFailure: 'Failed to select organization' + nextActions: + - command: 'tigris ls' + description: 'List your buckets' + - command: 'tigris buckets create ' + description: 'Create a new bucket' arguments: - name: name type: positional @@ -689,6 +710,11 @@ commands: onStart: 'Creating bucket...' onSuccess: "Bucket '{{name}}' created successfully" onFailure: 'Failed to create bucket' + nextActions: + - command: 'tigris access-keys create ' + description: 'Create an access key for programmatic access' + - command: 'tigris cp ./file t3://{{name}}/' + description: 'Upload files to the new bucket' arguments: - name: name description: Name of the bucket @@ -779,6 +805,9 @@ commands: onStart: 'Deleting bucket...' onSuccess: "Bucket '{{name}}' deleted successfully" onFailure: "Failed to delete bucket '{{name}}'" + nextActions: + - command: 'tigris ls' + description: 'List remaining buckets' arguments: - name: name description: Name of the bucket or comma separated list of buckets @@ -1289,6 +1318,9 @@ commands: onStart: 'Uploading object...' onSuccess: "Object '{{key}}' uploaded successfully" onFailure: 'Failed to upload object' + nextActions: + - command: 'tigris presign {{bucket}}/{{key}}' + description: 'Generate a presigned URL for the uploaded object' arguments: - name: bucket description: Name of the bucket @@ -1334,6 +1366,9 @@ commands: onStart: 'Deleting object...' onSuccess: "Object '{{key}}' deleted successfully" onFailure: 'Failed to delete object' + nextActions: + - command: 'tigris ls {{bucket}}' + description: 'List remaining objects in the bucket' arguments: - name: bucket description: Name of the bucket @@ -1435,6 +1470,9 @@ commands: onStart: 'Creating access key...' onSuccess: 'Access key created' onFailure: 'Failed to create access key' + nextActions: + - command: 'tigris access-keys assign {{id}} --bucket --role Editor' + description: 'Assign bucket roles to the new access key' arguments: - name: name description: Name for the access key @@ -1513,6 +1551,9 @@ commands: onStart: 'Assigning bucket roles...' onSuccess: 'Bucket roles assigned' onFailure: 'Failed to assign bucket roles' + nextActions: + - command: 'tigris cp ./file t3:///' + description: 'Upload files using the assigned access key' arguments: - name: id description: Access key ID diff --git a/src/types.ts b/src/types.ts index adb0efa..f3ef84d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,11 @@ export interface Argument { examples?: string[]; } +export interface NextAction { + command: string; + description: string; +} + export interface Messages { onStart?: string; onSuccess?: string; @@ -21,6 +26,7 @@ export interface Messages { onAlreadyDone?: string; onDeprecated?: string; hint?: string; + nextActions?: NextAction[]; } // Recursive command structure - supports nth level nesting diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..2b37dcc --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,181 @@ +import type { NextAction } from '../types.js'; + +export enum ExitCode { + Success = 0, + GeneralError = 1, + AuthFailure = 2, + NotFound = 3, + RateLimit = 4, + NetworkError = 5, +} + +export type ErrorCategory = + | 'auth' + | 'permission' + | 'not_found' + | 'rate_limit' + | 'network' + | 'general'; + +export interface ClassifiedError { + exitCode: ExitCode; + category: ErrorCategory; + message: string; + nextActions: NextAction[]; +} + +// Pattern groups ordered by priority (auth > permission > not_found > rate_limit > network > general) +// AUTH = not logged in at all; PERMISSION = logged in but lacks access to resource +const AUTH_PATTERNS: RegExp[] = [ + /not authenticated/i, + /no organization selected/i, + /token refresh failed/i, + /please run "tigris login/i, + /logged in via OAuth/i, +]; + +const PERMISSION_PATTERNS: RegExp[] = [/access denied/i, /forbidden/i]; + +const NOT_FOUND_PATTERNS: RegExp[] = [ + /NoSuchBucket/, + /NoSuchKey/, + /bucket not found/i, + /object not found/i, + /resource .+ does not exist/i, + /the specified key does not exist/i, +]; + +const RATE_LIMIT_PATTERNS: RegExp[] = [ + /rate limit/i, + /too many requests/i, + /throttl/i, + /SlowDown/, +]; + +const NETWORK_PATTERNS: RegExp[] = [ + /ECONNREFUSED/, + /ENOTFOUND/, + /ETIMEDOUT/, + /ECONNRESET/, + /socket hang up/i, + /fetch failed/i, +]; + +function matchesAny(message: string, patterns: RegExp[]): boolean { + return patterns.some((p) => p.test(message)); +} + +function getAuthNextActions(): NextAction[] { + return [ + { command: 'tigris login', description: 'Authenticate via OAuth' }, + { + command: 'tigris configure', + description: 'Set up access key credentials', + }, + ]; +} + +function getPermissionNextActions(): NextAction[] { + return [ + { + command: 'tigris access-keys list', + description: 'Check your access key permissions', + }, + { command: 'tigris login', description: 'Re-authenticate if needed' }, + ]; +} + +function getNotFoundNextActions(): NextAction[] { + return [{ command: 'tigris ls', description: 'List available buckets' }]; +} + +function getRateLimitNextActions(): NextAction[] { + return []; +} + +function getNetworkNextActions(): NextAction[] { + return [ + { + command: 'tigris credentials test', + description: 'Test connectivity and credentials', + }, + ]; +} + +/** + * Classify an error by pattern-matching its message. + * Returns a ClassifiedError with the appropriate exit code, category, + * and suggested next actions for agents. + */ +function hasMessage(error: unknown): error is { message: string } { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof error.message === 'string' + ); +} + +function extractMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if (hasMessage(error)) return error.message; + if (typeof error === 'string') return error; + if (error === null || error === undefined) return 'Unknown error'; + return String(error); +} + +export function classifyError(error: unknown): ClassifiedError { + const message = extractMessage(error); + + if (matchesAny(message, AUTH_PATTERNS)) { + return { + exitCode: ExitCode.AuthFailure, + category: 'auth', + message, + nextActions: getAuthNextActions(), + }; + } + + if (matchesAny(message, PERMISSION_PATTERNS)) { + return { + exitCode: ExitCode.AuthFailure, + category: 'permission', + message, + nextActions: getPermissionNextActions(), + }; + } + + if (matchesAny(message, NOT_FOUND_PATTERNS)) { + return { + exitCode: ExitCode.NotFound, + category: 'not_found', + message, + nextActions: getNotFoundNextActions(), + }; + } + + if (matchesAny(message, RATE_LIMIT_PATTERNS)) { + return { + exitCode: ExitCode.RateLimit, + category: 'rate_limit', + message, + nextActions: getRateLimitNextActions(), + }; + } + + if (matchesAny(message, NETWORK_PATTERNS)) { + return { + exitCode: ExitCode.NetworkError, + category: 'network', + message, + nextActions: getNetworkNextActions(), + }; + } + + return { + exitCode: ExitCode.GeneralError, + category: 'general', + message, + nextActions: [], + }; +} diff --git a/src/utils/exit.ts b/src/utils/exit.ts new file mode 100644 index 0000000..b846977 --- /dev/null +++ b/src/utils/exit.ts @@ -0,0 +1,90 @@ +import { classifyError } from './errors.js'; +import type { NextAction } from '../types.js'; +import { getCommandSpec } from './specs.js'; +import { interpolate } from './messages.js'; +import type { MessageContext, MessageVariables } from './messages.js'; + +function isJsonMode(): boolean { + return globalThis.__TIGRIS_JSON_MODE === true; +} + +function isStderrTTY(): boolean { + return process.stderr.isTTY === true; +} + +function isStdoutTTY(): boolean { + return process.stdout.isTTY === true; +} + +/** + * Exit with a classified error code. + * - JSON mode: outputs structured error JSON to stderr + * - Non-JSON without context: prints the error message to stderr + * (callers that pass context already printed via printFailure) + * - TTY mode: prints "Next steps:" hints to stderr + * - Always exits with the classified exit code + */ +export function exitWithError(error: unknown, context?: MessageContext): never { + const classified = classifyError(error); + + if (isJsonMode()) { + const errorOutput: Record = { + error: { + message: classified.message, + code: classified.exitCode, + category: classified.category, + }, + }; + if (classified.nextActions.length > 0) { + errorOutput.nextActions = classified.nextActions; + } + console.error(JSON.stringify(errorOutput)); + } else { + if (!context) { + console.error(`\nError: ${classified.message}`); + } + if (isStderrTTY() && classified.nextActions.length > 0) { + console.error('\nNext steps:'); + for (const action of classified.nextActions) { + console.error(` → ${action.command} ${action.description}`); + } + } + } + + process.exit(classified.exitCode); +} + +/** + * Read nextActions from specs.yaml for a command and interpolate variables. + * Returns empty array if no nextActions defined. + */ +export function getSuccessNextActions( + context: MessageContext, + variables?: MessageVariables +): NextAction[] { + const spec = getCommandSpec(context.command, context.operation); + if (!spec?.messages?.nextActions) return []; + + return spec.messages.nextActions.map((action) => ({ + command: interpolate(action.command, variables), + description: interpolate(action.description, variables), + })); +} + +/** + * Print "Next steps:" hints for success cases. + * Only prints in TTY mode and only when nextActions are defined. + */ +export function printNextActions( + context: MessageContext, + variables?: MessageVariables +): void { + if (!isStdoutTTY() || isJsonMode()) return; + const nextActions = getSuccessNextActions(context, variables); + if (nextActions.length === 0) return; + + console.log('\nNext steps:'); + for (const action of nextActions) { + console.log(` → ${action.command} ${action.description}`); + } +} diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 196c7cd..5c49ecb 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -37,7 +37,10 @@ function getMessages(context: MessageContext): Messages | undefined { * Supports {{variableName}} syntax * Also processes \n for multiline support */ -function interpolate(template: string, variables?: MessageVariables): string { +export function interpolate( + template: string, + variables?: MessageVariables +): string { let result = template; // Process escaped newlines for multiline support @@ -88,12 +91,14 @@ export function printSuccess( /** * Print the onFailure message for a command/operation + * Suppressed in JSON mode to avoid mixing human-readable text with structured JSON on stderr */ export function printFailure( context: MessageContext, error?: string, variables?: MessageVariables ): void { + if (globalThis.__TIGRIS_JSON_MODE === true) return; const messages = getMessages(context); if (messages?.onFailure) { console.error( diff --git a/test/specs-completeness.test.ts b/test/specs-completeness.test.ts index 0f5d12f..943e9cb 100644 --- a/test/specs-completeness.test.ts +++ b/test/specs-completeness.test.ts @@ -141,6 +141,38 @@ describe('specs completeness', () => { } }); + describe('nextActions entries have command and description', () => { + const withNextActions = allCommands.filter( + ({ spec }) => + spec.messages && + (spec.messages as Record).nextActions + ); + + if (withNextActions.length === 0) { + it('no commands with nextActions found (skip)', () => { + expect(true).toBe(true); + }); + } + + for (const { spec, path } of withNextActions) { + const label = path.join(' '); + it(`${label}`, () => { + const nextActions = (spec.messages as Record) + .nextActions as Array>; + expect(Array.isArray(nextActions)).toBe(true); + expect(nextActions.length).toBeGreaterThan(0); + for (const action of nextActions) { + expect(action).toHaveProperty('command'); + expect(action).toHaveProperty('description'); + expect(typeof action.command).toBe('string'); + expect(typeof action.description).toBe('string'); + expect((action.command as string).length).toBeGreaterThan(0); + expect((action.description as string).length).toBeGreaterThan(0); + } + }); + } + }); + describe('deprecated commands have onDeprecated message', () => { const deprecated = allCommands.filter(({ spec }) => spec.deprecated); diff --git a/test/utils/errors.test.ts b/test/utils/errors.test.ts new file mode 100644 index 0000000..2fa4eac --- /dev/null +++ b/test/utils/errors.test.ts @@ -0,0 +1,216 @@ +import { describe, it, expect } from 'vitest'; +import { + classifyError, + ExitCode, + type ClassifiedError, +} from '../../src/utils/errors.js'; + +describe('classifyError', () => { + describe('auth errors (exit code 2)', () => { + const authMessages = [ + 'not authenticated', + 'Not Authenticated - please login', + 'No organization selected', + 'Token refresh failed', + 'Please run "tigris login" to authenticate', + 'Policies can only be created when logged in via OAuth.', + 'Users can only be invited when logged in via OAuth.\nRun "tigris login oauth" first.', + ]; + + for (const msg of authMessages) { + it(`classifies "${msg}" as auth`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.AuthFailure); + expect(result.category).toBe('auth'); + expect(result.nextActions.length).toBeGreaterThan(0); + expect(result.nextActions.some((a) => a.command.includes('login'))).toBe( + true + ); + }); + } + }); + + describe('permission errors (exit code 2)', () => { + const permissionMessages = [ + 'Access Denied', + 'access denied to resource', + 'Forbidden', + '403 Forbidden', + ]; + + for (const msg of permissionMessages) { + it(`classifies "${msg}" as permission`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.AuthFailure); + expect(result.category).toBe('permission'); + expect(result.nextActions.length).toBeGreaterThan(0); + expect( + result.nextActions.some((a) => a.command.includes('access-keys')) + ).toBe(true); + }); + } + }); + + describe('not found errors (exit code 3)', () => { + const notFoundMessages = [ + 'Bucket not found', + 'NoSuchBucket', + 'NoSuchKey', + 'Resource xyz does not exist', + 'The specified key does not exist', + 'Object not found in bucket', + ]; + + for (const msg of notFoundMessages) { + it(`classifies "${msg}" as not_found`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.NotFound); + expect(result.category).toBe('not_found'); + expect(result.nextActions.length).toBeGreaterThan(0); + expect(result.nextActions.some((a) => a.command.includes('ls'))).toBe( + true + ); + }); + } + }); + + describe('rate limit errors (exit code 4)', () => { + const rateLimitMessages = [ + 'Rate limit exceeded', + 'Too many requests', + 'Request throttled', + 'SlowDown', + ]; + + for (const msg of rateLimitMessages) { + it(`classifies "${msg}" as rate_limit`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.RateLimit); + expect(result.category).toBe('rate_limit'); + expect(result.nextActions).toEqual([]); + }); + } + }); + + describe('network errors (exit code 5)', () => { + const networkMessages = [ + 'connect ECONNREFUSED 127.0.0.1:443', + 'getaddrinfo ENOTFOUND api.example.com', + 'connect ETIMEDOUT 1.2.3.4:443', + 'read ECONNRESET', + 'socket hang up', + 'fetch failed', + ]; + + for (const msg of networkMessages) { + it(`classifies "${msg}" as network`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.NetworkError); + expect(result.category).toBe('network'); + expect(result.nextActions.length).toBeGreaterThan(0); + expect( + result.nextActions.some((a) => + a.command.includes('credentials test') + ) + ).toBe(true); + }); + } + }); + + describe('general errors (exit code 1)', () => { + const generalMessages = [ + 'Bucket name is required', + 'Invalid argument', + 'Something unexpected happened', + 'Source not found: ./myfile', + 'File not found: ./report.pdf', + '', + ]; + + for (const msg of generalMessages) { + it(`classifies "${msg || '(empty)'}" as general`, () => { + const result = classifyError(new Error(msg)); + expect(result.exitCode).toBe(ExitCode.GeneralError); + expect(result.category).toBe('general'); + expect(result.nextActions).toEqual([]); + }); + } + }); + + describe('priority ordering', () => { + it('auth takes priority over permission when both match', () => { + // "not authenticated, access denied" matches both auth and permission + const result = classifyError( + new Error('not authenticated, access denied') + ); + expect(result.exitCode).toBe(ExitCode.AuthFailure); + expect(result.category).toBe('auth'); + }); + + it('permission takes priority over not_found when both match', () => { + // "access denied: resource not found" matches both permission and not_found + const result = classifyError( + new Error('access denied: resource not found') + ); + expect(result.exitCode).toBe(ExitCode.AuthFailure); + expect(result.category).toBe('permission'); + }); + }); + + describe('input types', () => { + it('handles Error objects', () => { + const result = classifyError(new Error('NoSuchBucket')); + expect(result.exitCode).toBe(ExitCode.NotFound); + }); + + it('handles plain objects with message property (SDK errors)', () => { + const sdkError = { message: 'NoSuchBucket', code: 'NoSuchBucket' }; + const result = classifyError(sdkError); + expect(result.exitCode).toBe(ExitCode.NotFound); + expect(result.message).toBe('NoSuchBucket'); + }); + + it('handles string errors', () => { + const result = classifyError('NoSuchBucket'); + expect(result.exitCode).toBe(ExitCode.NotFound); + }); + + it('handles undefined', () => { + const result = classifyError(undefined); + expect(result.exitCode).toBe(ExitCode.GeneralError); + expect(result.message).toBe('Unknown error'); + }); + + it('handles null', () => { + const result = classifyError(null); + expect(result.exitCode).toBe(ExitCode.GeneralError); + }); + + it('handles number', () => { + const result = classifyError(42); + expect(result.exitCode).toBe(ExitCode.GeneralError); + expect(result.message).toBe('42'); + }); + }); + + describe('ClassifiedError structure', () => { + it('always has all required fields', () => { + const result: ClassifiedError = classifyError(new Error('test')); + expect(result).toHaveProperty('exitCode'); + expect(result).toHaveProperty('category'); + expect(result).toHaveProperty('message'); + expect(result).toHaveProperty('nextActions'); + expect(Array.isArray(result.nextActions)).toBe(true); + }); + + it('nextActions have command and description', () => { + const result = classifyError(new Error('access denied')); + for (const action of result.nextActions) { + expect(action).toHaveProperty('command'); + expect(action).toHaveProperty('description'); + expect(typeof action.command).toBe('string'); + expect(typeof action.description).toBe('string'); + } + }); + }); +}); diff --git a/test/utils/exit.test.ts b/test/utils/exit.test.ts new file mode 100644 index 0000000..0a1db3a --- /dev/null +++ b/test/utils/exit.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import * as YAML from 'yaml'; +import { setSpecs } from '../../src/utils/specs.js'; +import { + exitWithError, + getSuccessNextActions, + printNextActions, +} from '../../src/utils/exit.js'; +import { ExitCode } from '../../src/utils/errors.js'; + +// Pre-populate specs cache +const specsYaml = readFileSync( + join(process.cwd(), 'src', 'specs.yaml'), + 'utf8' +); +setSpecs(YAML.parse(specsYaml, { schema: 'core' })); + +// Save original TTY descriptors +const originalStdoutIsTTY = Object.getOwnPropertyDescriptor( + process.stdout, + 'isTTY' +); +const originalStderrIsTTY = Object.getOwnPropertyDescriptor( + process.stderr, + 'isTTY' +); + +function setStdoutTTY(value: boolean) { + Object.defineProperty(process.stdout, 'isTTY', { + value, + writable: true, + configurable: true, + }); +} + +function setStderrTTY(value: boolean) { + Object.defineProperty(process.stderr, 'isTTY', { + value, + writable: true, + configurable: true, + }); +} + +function restoreTTY() { + if (originalStdoutIsTTY) { + Object.defineProperty(process.stdout, 'isTTY', originalStdoutIsTTY); + } else { + delete (process.stdout as unknown as Record).isTTY; + } + if (originalStderrIsTTY) { + Object.defineProperty(process.stderr, 'isTTY', originalStderrIsTTY); + } else { + delete (process.stderr as unknown as Record).isTTY; + } +} + +function setJsonMode(value: boolean) { + globalThis.__TIGRIS_JSON_MODE = value; +} + +describe('exitWithError', () => { + let errorSpy: ReturnType; + let exitSpy: ReturnType; + + beforeEach(() => { + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + setJsonMode(false); + }); + + afterEach(() => { + errorSpy.mockRestore(); + exitSpy.mockRestore(); + restoreTTY(); + setJsonMode(false); + }); + + it('exits with classified code for auth errors', () => { + exitWithError(new Error('access denied')); + expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthFailure); + }); + + it('exits with classified code for not-found errors', () => { + exitWithError(new Error('NoSuchBucket')); + expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound); + }); + + it('exits with classified code for rate limit errors', () => { + exitWithError(new Error('rate limit exceeded')); + expect(exitSpy).toHaveBeenCalledWith(ExitCode.RateLimit); + }); + + it('exits with classified code for network errors', () => { + exitWithError(new Error('ECONNREFUSED')); + expect(exitSpy).toHaveBeenCalledWith(ExitCode.NetworkError); + }); + + it('exits with code 1 for general errors', () => { + exitWithError(new Error('something went wrong')); + expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError); + }); + + it('outputs structured JSON to stderr in JSON mode', () => { + setJsonMode(true); + exitWithError(new Error('access denied')); + + const jsonCalls = errorSpy.mock.calls.filter((call) => { + try { + JSON.parse(call[0] as string); + return true; + } catch { + return false; + } + }); + expect(jsonCalls.length).toBe(1); + + const output = JSON.parse(jsonCalls[0][0] as string); + expect(output).toHaveProperty('error'); + expect(output.error).toHaveProperty('message', 'access denied'); + expect(output.error).toHaveProperty('code', ExitCode.AuthFailure); + expect(output.error).toHaveProperty('category', 'permission'); + expect(output).toHaveProperty('nextActions'); + expect(output.nextActions.length).toBeGreaterThan(0); + }); + + it('omits nextActions from JSON when empty', () => { + setJsonMode(true); + exitWithError(new Error('something went wrong')); + + const jsonCalls = errorSpy.mock.calls.filter((call) => { + try { + JSON.parse(call[0] as string); + return true; + } catch { + return false; + } + }); + expect(jsonCalls.length).toBe(1); + + const output = JSON.parse(jsonCalls[0][0] as string); + expect(output).not.toHaveProperty('nextActions'); + }); + + it('prints next steps hints in TTY mode for classified errors', () => { + setStderrTTY(true); + exitWithError(new Error('access denied')); + + const allOutput = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allOutput).toContain('Next steps:'); + expect(allOutput).toContain('access-keys list'); + }); + + it('does not print next steps in non-TTY non-JSON mode', () => { + setStderrTTY(false); + setJsonMode(false); + exitWithError(new Error('access denied')); + + const allOutput = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allOutput).not.toContain('Next steps:'); + }); +}); + +describe('getSuccessNextActions', () => { + it('returns nextActions for a command that has them', () => { + const actions = getSuccessNextActions( + { command: 'buckets', operation: 'create' }, + { name: 'my-bucket' } + ); + expect(actions.length).toBeGreaterThan(0); + // Should interpolate {{name}} + expect(actions.some((a) => a.command.includes('my-bucket'))).toBe(true); + expect(actions.every((a) => !a.command.includes('{{name}}'))).toBe(true); + }); + + it('returns empty array for commands without nextActions', () => { + const actions = getSuccessNextActions({ + command: 'buckets', + operation: 'list', + }); + expect(actions).toEqual([]); + }); + + it('interpolates variables in command and description', () => { + const actions = getSuccessNextActions( + { command: 'organizations', operation: 'create' }, + { name: 'test-org' } + ); + expect(actions.length).toBeGreaterThan(0); + expect(actions[0].command).toContain('test-org'); + expect(actions[0].command).not.toContain('{{name}}'); + }); + + it('returns empty array for unknown command', () => { + const actions = getSuccessNextActions({ command: 'nonexistent' }); + expect(actions).toEqual([]); + }); +}); + +describe('printNextActions', () => { + let logSpy: ReturnType; + + beforeEach(() => { + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + restoreTTY(); + }); + + it('prints next steps in TTY mode', () => { + setStdoutTTY(true); + printNextActions( + { command: 'buckets', operation: 'create' }, + { name: 'my-bucket' } + ); + + const allOutput = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allOutput).toContain('Next steps:'); + expect(allOutput).toContain('my-bucket'); + }); + + it('is silent when not TTY', () => { + setStdoutTTY(false); + printNextActions( + { command: 'buckets', operation: 'create' }, + { name: 'my-bucket' } + ); + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('is silent when no nextActions defined', () => { + setStdoutTTY(true); + printNextActions({ command: 'buckets', operation: 'list' }); + expect(logSpy).not.toHaveBeenCalled(); + }); +}); From 6c365143262b9eabbdbb3f4c502ace9d2d07a403 Mon Sep 17 00:00:00 2001 From: Abdullah Ibrahim Date: Wed, 18 Mar 2026 09:33:23 +0000 Subject: [PATCH 7/7] feat: version command and minor fixes (#41) * fix: messages on --json mode * chore: package audit * feat: add version command --- package-lock.json | 1351 +++++++++++++++++++++-------------------- src/cli-core.ts | 7 + src/utils/messages.ts | 14 +- 3 files changed, 692 insertions(+), 680 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfd86b7..24ef051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,9 +79,9 @@ } }, "node_modules/@actions/http-client/node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "dev": true, "license": "MIT", "engines": { @@ -298,49 +298,49 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.1000.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1000.0.tgz", - "integrity": "sha512-7PtY49oxAo0rzkXZ1ulumtRL4QYi30Q5AMJtqJhYCHc1VZr0I2f0LHxiwovzquqUPzmTArgY6LjcPB7bkB/54w==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1011.0.tgz", + "integrity": "sha512-+B5jc1KuRLO7hNVCbA+BySNNe8+aclgVY+K+uwhrvbumMxK4RFi4VWMPliPq2u7HA4BwP/FhqjNu6zNjR/Y3Zg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/credential-provider-node": "^3.972.14", - "@aws-sdk/middleware-host-header": "^3.972.6", - "@aws-sdk/middleware-logger": "^3.972.6", - "@aws-sdk/middleware-recursion-detection": "^3.972.6", - "@aws-sdk/middleware-user-agent": "^3.972.15", - "@aws-sdk/region-config-resolver": "^3.972.6", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-endpoints": "^3.996.3", - "@aws-sdk/util-user-agent-browser": "^3.972.6", - "@aws-sdk/util-user-agent-node": "^3.973.0", - "@smithy/config-resolver": "^4.4.9", - "@smithy/core": "^3.23.6", - "@smithy/fetch-http-handler": "^5.3.11", - "@smithy/hash-node": "^4.2.10", - "@smithy/invalid-dependency": "^4.2.10", - "@smithy/middleware-content-length": "^4.2.10", - "@smithy/middleware-endpoint": "^4.4.20", - "@smithy/middleware-retry": "^4.4.37", - "@smithy/middleware-serde": "^4.2.11", - "@smithy/middleware-stack": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/node-http-handler": "^4.4.12", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-body-length-node": "^4.2.2", - "@smithy/util-defaults-mode-browser": "^4.3.36", - "@smithy/util-defaults-mode-node": "^4.2.39", - "@smithy/util-endpoints": "^3.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-retry": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@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.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -348,65 +348,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1006.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1006.0.tgz", - "integrity": "sha512-tm8R/LgWDC3zWPMCdD990owvBrmuIM2A39+OWKW/HyAomWi6ancPz/H1K/hmxf0bqdXAaRUHBQMAmzwb1aR33Q==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1011.0.tgz", + "integrity": "sha512-jY7CGX+vfM/DSi4K8UwaZKoXnhqchmAbKFB1kIuHMfPPqW7l3jC/fUVDb95/njMsB2ymYOTusZEzoCTeUB/4qA==", "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.973.19", - "@aws-sdk/credential-provider-node": "^3.972.19", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", - "@aws-sdk/middleware-expect-continue": "^3.972.7", - "@aws-sdk/middleware-flexible-checksums": "^3.973.5", - "@aws-sdk/middleware-host-header": "^3.972.7", - "@aws-sdk/middleware-location-constraint": "^3.972.7", - "@aws-sdk/middleware-logger": "^3.972.7", - "@aws-sdk/middleware-recursion-detection": "^3.972.7", - "@aws-sdk/middleware-sdk-s3": "^3.972.19", - "@aws-sdk/middleware-ssec": "^3.972.7", - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/region-config-resolver": "^3.972.7", - "@aws-sdk/signature-v4-multi-region": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@aws-sdk/util-user-agent-browser": "^3.972.7", - "@aws-sdk/util-user-agent-node": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/core": "^3.23.9", - "@smithy/eventstream-serde-browser": "^4.2.11", - "@smithy/eventstream-serde-config-resolver": "^4.3.11", - "@smithy/eventstream-serde-node": "^4.2.11", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/hash-blob-browser": "^4.2.12", - "@smithy/hash-node": "^4.2.11", - "@smithy/hash-stream-node": "^4.2.11", - "@smithy/invalid-dependency": "^4.2.11", - "@smithy/md5-js": "^4.2.11", - "@smithy/middleware-content-length": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-retry": "^4.4.40", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", + "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/middleware-flexible-checksums": "^3.974.0", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-location-constraint": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/middleware-ssec": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-blob-browser": "^4.2.13", + "@smithy/hash-node": "^4.2.12", + "@smithy/hash-stream-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@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.39", - "@smithy/util-defaults-mode-node": "^4.2.42", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.19", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.12", + "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -414,22 +414,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.19.tgz", - "integrity": "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/xml-builder": "^3.972.10", - "@smithy/core": "^3.23.9", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -438,12 +438,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.4.tgz", - "integrity": "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -451,15 +451,15 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.6.tgz", - "integrity": "sha512-RJqEZYFoXkBTVCwSJuYFd311qc/Q/cBJ8BH08+ggX/rUTWw47TUEyZlxzyTlKfP7DoXG4Khu/TX+pzU6godEGQ==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.13.tgz", + "integrity": "sha512-WZnIK8NPX+4OXkpVoNmUS+Ya1osqjszUsDqFEz97+a/LD5K012np9iR/eWEC43btx8zQjyRIK8kyiwbh8SiHzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -467,15 +467,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.17.tgz", - "integrity": "sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -483,20 +483,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.19.tgz", - "integrity": "sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.17", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" }, "engines": { @@ -504,24 +504,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.18.tgz", - "integrity": "sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/credential-provider-env": "^3.972.17", - "@aws-sdk/credential-provider-http": "^3.972.19", - "@aws-sdk/credential-provider-login": "^3.972.18", - "@aws-sdk/credential-provider-process": "^3.972.17", - "@aws-sdk/credential-provider-sso": "^3.972.18", - "@aws-sdk/credential-provider-web-identity": "^3.972.18", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -529,18 +529,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.18.tgz", - "integrity": "sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -548,22 +548,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.19.tgz", - "integrity": "sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.17", - "@aws-sdk/credential-provider-http": "^3.972.19", - "@aws-sdk/credential-provider-ini": "^3.972.18", - "@aws-sdk/credential-provider-process": "^3.972.17", - "@aws-sdk/credential-provider-sso": "^3.972.18", - "@aws-sdk/credential-provider-web-identity": "^3.972.18", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -571,16 +571,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.17.tgz", - "integrity": "sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -588,18 +588,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.18.tgz", - "integrity": "sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/token-providers": "3.1005.0", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -607,17 +607,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.18.tgz", - "integrity": "sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -625,30 +625,30 @@ } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.1000.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1000.0.tgz", - "integrity": "sha512-J0pBgTZ2b3UCnj+NQTPtWYjrEUne2aGwq1Xuuw8P2cIMpPBYJc39e59oYoRGpNseUXqcjkh0nLtWqZREEeMvkg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.1000.0", - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.6", - "@aws-sdk/credential-provider-env": "^3.972.13", - "@aws-sdk/credential-provider-http": "^3.972.15", - "@aws-sdk/credential-provider-ini": "^3.972.13", - "@aws-sdk/credential-provider-login": "^3.972.13", - "@aws-sdk/credential-provider-node": "^3.972.14", - "@aws-sdk/credential-provider-process": "^3.972.13", - "@aws-sdk/credential-provider-sso": "^3.972.13", - "@aws-sdk/credential-provider-web-identity": "^3.972.13", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/config-resolver": "^4.4.9", - "@smithy/core": "^3.23.6", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1011.0.tgz", + "integrity": "sha512-Ev3ZOqB+vBdg5WoKbyg/FQqXoICM+H5OoL7qPqbiww3LGIVuKbxbTBUtxhOynnyiKt1yD6rIP6KxL3XjXxTAXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1011.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.13", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -656,14 +656,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1006.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1006.0.tgz", - "integrity": "sha512-oc7GkB4hN6DBJgbTr4l8Hvv4MXJYUU1lhnUEQnXNiOpr5VhLBtZLxZVb4HotPKGVNvCPDKoJ8gxpy15EjUAS8A==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1011.0.tgz", + "integrity": "sha512-bUWxlwTbFZoXBhLSFaeQjee0d4aI/eUk6Ug9FLoRP8/bNaiUFldAABpfb/OHc0G433ZgqHSmhnpnAzOtV+8mBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/smithy-client": "^4.12.3", + "@smithy/abort-controller": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/smithy-client": "^4.12.5", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", @@ -673,20 +673,20 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1006.0" + "@aws-sdk/client-s3": "^3.1011.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.7.tgz", - "integrity": "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", + "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -695,14 +695,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.7.tgz", - "integrity": "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", + "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -710,23 +710,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.5.tgz", - "integrity": "sha512-Dp3hqE5W6hG8HQ3Uh+AINx9wjjqYmFHbxede54sGj3akx/haIQrkp85lNdTdC+ouNUcSYNiuGkzmyDREfHX1Gg==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", + "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", "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.973.19", - "@aws-sdk/crc64-nvme": "^3.972.4", - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/crc64-nvme": "^3.972.5", + "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -735,14 +735,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", - "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -750,13 +750,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.7.tgz", - "integrity": "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", + "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -764,13 +764,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", - "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -778,15 +778,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", - "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -794,23 +794,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.19.tgz", - "integrity": "sha512-/CtOHHVFg4ZuN6CnLnYkrqWgVEnbOBC4kNiKa+4fldJ9cioDt3dD/f5vpq0cWLOXwmGL2zgVrVxNhjxWpxNMkg==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", + "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.9", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -819,13 +819,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.7.tgz", - "integrity": "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", + "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -833,18 +833,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.20.tgz", - "integrity": "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@smithy/core": "^3.23.9", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-retry": "^4.2.11", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -852,47 +852,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.8.tgz", - "integrity": "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==", + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/middleware-host-header": "^3.972.7", - "@aws-sdk/middleware-logger": "^3.972.7", - "@aws-sdk/middleware-recursion-detection": "^3.972.7", - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/region-config-resolver": "^3.972.7", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@aws-sdk/util-user-agent-browser": "^3.972.7", - "@aws-sdk/util-user-agent-node": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/core": "^3.23.9", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/hash-node": "^4.2.11", - "@smithy/invalid-dependency": "^4.2.11", - "@smithy/middleware-content-length": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-retry": "^4.4.40", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@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.39", - "@smithy/util-defaults-mode-node": "^4.2.42", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -901,15 +901,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", - "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -917,18 +917,18 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1006.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1006.0.tgz", - "integrity": "sha512-azZTNllb6zq3hyGvTViBflfN5IeThmSQbYB+JJJqVGB9ZAqV9d6xOUG1BFCtxoKukDT9JnUZqQwQB0Y24gJAPw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-format-url": "^3.972.7", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1011.0.tgz", + "integrity": "sha512-Jbh8hIxgfiskZNC9Sb3aDmFCuYkNyVxlYHXx4zZEzSwEx+duWz/BSb1aJv9FiZIzFgCMK/Vh7HqGnJ9DEYipEw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-format-url": "^3.972.8", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -936,16 +936,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.7.tgz", - "integrity": "sha512-mYhh7FY+7OOqjkYkd6+6GgJOsXK1xBWmuR+c5mxJPj2kr5TBNeZq+nUvE9kANWAux5UxDVrNOSiEM/wlHzC3Lg==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.8.tgz", + "integrity": "sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -953,17 +953,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1005.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1005.0.tgz", - "integrity": "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==", + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -971,12 +971,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -996,15 +996,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", - "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-endpoints": "^3.3.2", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -1012,14 +1012,14 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.7.tgz", - "integrity": "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", + "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -1039,27 +1039,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", - "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.5.tgz", - "integrity": "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/types": "^3.973.5", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -1075,12 +1076,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", - "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, @@ -1089,9 +1090,9 @@ } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", - "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -3201,12 +3202,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", - "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3239,16 +3240,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", - "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -3256,18 +3257,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.9", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz", - "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==", + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.12", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -3277,15 +3278,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", - "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -3293,13 +3294,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", - "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -3308,13 +3309,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", - "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3322,12 +3323,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", - "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3335,13 +3336,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", - "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3349,13 +3350,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", - "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3363,14 +3364,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", - "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -3379,14 +3380,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", - "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", + "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3394,12 +3395,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", - "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -3409,12 +3410,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", - "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", + "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -3423,12 +3424,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", - "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3448,12 +3449,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", - "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -3462,13 +3463,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", - "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3476,18 +3477,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.23", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz", - "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.9", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-middleware": "^4.2.11", + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -3495,18 +3496,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.40", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz", - "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/service-error-classification": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -3515,13 +3516,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", - "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3529,12 +3531,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", - "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3542,14 +3544,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", - "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3557,15 +3559,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", - "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3573,12 +3575,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", - "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3586,12 +3588,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", - "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3599,12 +3601,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", - "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -3613,12 +3615,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", - "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3626,24 +3628,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", - "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0" + "@smithy/types": "^4.13.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", - "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3651,16 +3653,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", - "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -3670,17 +3672,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz", - "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==", + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.9", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.17", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -3688,9 +3690,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3700,13 +3702,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", - "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3777,14 +3779,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.39", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz", - "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==", + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3792,17 +3794,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz", - "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==", + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.10", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3810,13 +3812,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", - "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3836,12 +3838,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", - "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3849,13 +3851,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", - "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -3863,14 +3865,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.17", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", - "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/types": "^4.13.0", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -3907,13 +3909,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.12.tgz", - "integrity": "sha512-ek5hyDrzS6mBFsNCEX8LpM+EWSLq6b9FdmPRlkpXXhiJE6aIZehKT9clC6+nFpZAA+i/Yg0xlaPeWGNbf5rzQA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", + "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5924,9 +5926,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", - "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", @@ -6073,9 +6075,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7507,9 +7509,9 @@ } }, "node_modules/npm": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.11.0.tgz", - "integrity": "sha512-82gRxKrh/eY5UnNorkTFcdBQAGpgjWehkfGVqAGlJjejEtJZGGJUqjo3mbBTNbc5BTnPKGVtGPBZGhElujX5cw==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.11.1.tgz", + "integrity": "sha512-asazCodkFdz1ReQzukyzS/DD77uGCIqUFeRG3gtaT8b9UR0ne1m9QOBuMgT72ij1rt7TRrOox4A1WzntMWIuEg==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -7588,7 +7590,7 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.4.0", + "@npmcli/arborist": "^9.4.1", "@npmcli/config": "^10.7.1", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", @@ -7596,7 +7598,7 @@ "@npmcli/package-json": "^7.0.5", "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.3", + "@npmcli/run-script": "^10.0.4", "@sigstore/tuf": "^4.0.1", "abbrev": "^4.0.0", "archy": "~1.0.0", @@ -7613,17 +7615,17 @@ "is-cidr": "^6.0.3", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.3", - "libnpmexec": "^10.2.3", - "libnpmfund": "^7.0.17", + "libnpmdiff": "^8.1.4", + "libnpmexec": "^10.2.4", + "libnpmfund": "^7.0.18", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.3", + "libnpmpack": "^9.1.4", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.3", "make-fetch-happen": "^15.0.4", - "minimatch": "^10.2.2", + "minimatch": "^10.2.4", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", @@ -7637,7 +7639,7 @@ "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.4.0", + "pacote": "^21.5.0", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", @@ -7646,7 +7648,7 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.9", + "tar": "^7.5.11", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", @@ -7730,11 +7732,12 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.4.0", + "version": "9.4.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", @@ -7943,7 +7946,7 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "10.0.3", + "version": "10.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -7952,8 +7955,7 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -8123,7 +8125,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.4", "dev": true, "inBundle": true, "license": "MIT", @@ -8513,12 +8515,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.3", + "version": "8.1.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.0", + "@npmcli/arborist": "^9.4.1", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -8532,13 +8534,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.2.3", + "version": "10.2.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.4.0", + "@npmcli/arborist": "^9.4.1", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -8555,12 +8557,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.17", + "version": "7.0.18", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.0" + "@npmcli/arborist": "^9.4.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -8580,12 +8582,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.3", + "version": "9.1.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.0", + "@npmcli/arborist": "^9.4.1", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -8686,7 +8688,7 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "10.2.2", + "version": "10.2.4", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -9024,7 +9026,7 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.4.0", + "version": "21.5.0", "dev": true, "inBundle": true, "license": "ISC", @@ -9329,7 +9331,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.9", + "version": "7.5.11", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -9488,12 +9490,11 @@ } }, "node_modules/npm/node_modules/write-file-atomic": { - "version": "7.0.0", + "version": "7.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" }, "engines": { @@ -11711,9 +11712,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "dev": true, "license": "MIT", "engines": { diff --git a/src/cli-core.ts b/src/cli-core.ts index bfcd2f8..a128486 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -527,6 +527,13 @@ export function createProgram(config: CLIConfig): CommanderCommand { showMainHelp(specs, version, hasImplementation); }); + program + .command('version') + .description('Show the CLI version') + .action(() => { + console.log(version); + }); + program.action(() => { showMainHelp(specs, version, hasImplementation); }); diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 5c49ecb..24deec4 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -26,6 +26,10 @@ function isTTY(): boolean { return process.stdout.isTTY === true; } +function isJsonMode(): boolean { + return globalThis.__TIGRIS_JSON_MODE === true; +} + function getMessages(context: MessageContext): Messages | undefined { const spec = getCommandSpec(context.command, context.operation); if (!spec) return undefined; @@ -65,7 +69,7 @@ export function printStart( context: MessageContext, variables?: MessageVariables ): void { - if (!isTTY()) return; + if (!isTTY() || isJsonMode()) return; const messages = getMessages(context); if (messages?.onStart) { console.log(interpolate(messages.onStart, variables)); @@ -80,7 +84,7 @@ export function printSuccess( context: MessageContext, variables?: MessageVariables ): void { - if (!isTTY()) return; + if (!isTTY() || isJsonMode()) return; const messages = getMessages(context); if (messages?.onSuccess) { console.log( @@ -118,7 +122,7 @@ export function printEmpty( context: MessageContext, variables?: MessageVariables ): void { - if (!isTTY()) return; + if (!isTTY() || isJsonMode()) return; const messages = getMessages(context); if (messages?.onEmpty) { console.log(interpolate(messages.onEmpty, variables)); @@ -133,7 +137,7 @@ export function printAlreadyDone( context: MessageContext, variables?: MessageVariables ): void { - if (!isTTY()) return; + if (!isTTY() || isJsonMode()) return; const messages = getMessages(context); if (messages?.onAlreadyDone) { console.log(interpolate(messages.onAlreadyDone, variables)); @@ -148,7 +152,7 @@ export function printHint( context: MessageContext, variables?: MessageVariables ): void { - if (!isTTY()) return; + if (!isTTY() || isJsonMode()) return; const messages = getMessages(context); if (messages?.hint) { console.log(`${ICONS.hint} ${interpolate(messages.hint, variables)}`);