Skip to content

Commit f643218

Browse files
authored
fix: merge forking operations in bucket commands (#38)
1 parent db1c51d commit f643218

7 files changed

Lines changed: 120 additions & 9 deletions

File tree

src/cli-core.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { Command as CommanderCommand } from 'commander';
66
import type { Argument, CommandSpec, Specs } from './types.js';
7+
import { printDeprecated } from './utils/messages.js';
78

89
export interface ModuleLoader {
910
(commandPath: string[]): Promise<{
@@ -448,6 +449,10 @@ export function registerCommands(
448449
return;
449450
}
450451

452+
if (defaultCmd.deprecated && defaultCmd.messages?.onDeprecated) {
453+
printDeprecated(defaultCmd.messages.onDeprecated);
454+
}
455+
451456
await loadAndExecuteCommand(
452457
loadModule,
453458
[...currentPath, defaultCmd.name],
@@ -479,6 +484,10 @@ export function registerCommands(
479484
return;
480485
}
481486

487+
if (spec.deprecated && spec.messages?.onDeprecated) {
488+
printDeprecated(spec.messages.onDeprecated);
489+
}
490+
482491
await loadAndExecuteCommand(
483492
loadModule,
484493
currentPath,

src/lib/buckets/create.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export default async function create(options: Record<string, unknown>) {
3131
]);
3232
let defaultTier = getOption<string>(options, ['default-tier', 't', 'T']);
3333
let locations = getOption<string>(options, ['locations', 'l', 'L']);
34+
const forkOf = getOption<string>(options, ['fork-of', 'forkOf', 'fork']);
35+
const sourceSnapshot = getOption<string>(options, [
36+
'source-snapshot',
37+
'sourceSnapshot',
38+
'source-snap',
39+
]);
3440

3541
// Handle deprecated --region and --consistency options
3642
const deprecatedRegion = getOption<string>(options, ['region', 'r', 'R']);
@@ -125,11 +131,18 @@ export default async function create(options: Record<string, unknown>) {
125131
process.exit(1);
126132
}
127133

134+
if (sourceSnapshot && !forkOf) {
135+
printFailure(context, '--source-snapshot requires --fork-of');
136+
process.exit(1);
137+
}
138+
128139
const { error } = await createBucket(name, {
129140
defaultTier: (defaultTier ?? 'STANDARD') as StorageClass,
130141
enableSnapshot: enableSnapshots === true,
131142
access: (access ?? 'private') as 'public' | 'private',
132143
locations: parsedLocations ?? parseLocations(locations ?? 'global'),
144+
...(forkOf ? { sourceBucketName: forkOf } : {}),
145+
...(sourceSnapshot ? { sourceBucketSnapshot: sourceSnapshot } : {}),
133146
config: await getStorageConfig(),
134147
});
135148

src/lib/buckets/list.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getOption } from '../../utils/options.js';
22
import { formatOutput } from '../../utils/format.js';
33
import { getStorageConfig } from '../../auth/s3-client.js';
4-
import { listBuckets } from '@tigrisdata/storage';
4+
import { listBuckets, getBucketInfo } from '@tigrisdata/storage';
55
import {
66
printStart,
77
printSuccess,
@@ -17,10 +17,10 @@ export default async function list(options: Record<string, unknown>) {
1717

1818
try {
1919
const format = getOption<string>(options, ['format', 'F'], 'table');
20+
const forksOf = getOption<string>(options, ['forks-of', 'forksOf']);
21+
const config = await getStorageConfig();
2022

21-
const { data, error } = await listBuckets({
22-
config: await getStorageConfig(),
23-
});
23+
const { data, error } = await listBuckets({ config });
2424

2525
if (error) {
2626
printFailure(context, error.message);
@@ -32,6 +32,48 @@ export default async function list(options: Record<string, unknown>) {
3232
return;
3333
}
3434

35+
if (forksOf) {
36+
// Filter for forks of the named source bucket
37+
const { data: bucketInfo, error: infoError } = await getBucketInfo(
38+
forksOf,
39+
{ config }
40+
);
41+
42+
if (infoError) {
43+
printFailure(context, infoError.message);
44+
process.exit(1);
45+
}
46+
47+
if (!bucketInfo.hasForks) {
48+
printEmpty(context);
49+
return;
50+
}
51+
52+
const forks: Array<{ name: string; created: Date }> = [];
53+
54+
for (const bucket of data.buckets) {
55+
if (bucket.name === forksOf) continue;
56+
const { data: info } = await getBucketInfo(bucket.name, { config });
57+
if (info?.sourceBucketName === forksOf) {
58+
forks.push({ name: bucket.name, created: bucket.creationDate });
59+
}
60+
}
61+
62+
if (forks.length === 0) {
63+
printEmpty(context);
64+
return;
65+
}
66+
67+
const output = formatOutput(forks, format!, 'forks', 'fork', [
68+
{ key: 'name', header: 'Name' },
69+
{ key: 'created', header: 'Created' },
70+
]);
71+
72+
console.log(output);
73+
printSuccess(context, { count: forks.length });
74+
return;
75+
}
76+
3577
const buckets = data.buckets.map((bucket) => ({
3678
name: bucket.name,
3779
created: bucket.creationDate,

src/lib/mk.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export default async function mk(options: Record<string, unknown>) {
4040
'T',
4141
]);
4242
let locations = getOption<string>(options, ['locations', 'l', 'L']);
43+
const forkOf = getOption<string>(options, ['fork-of', 'forkOf', 'fork']);
44+
const sourceSnapshot = getOption<string>(options, [
45+
'source-snapshot',
46+
'sourceSnapshot',
47+
'source-snap',
48+
]);
4349

4450
// Handle deprecated --region and --consistency options
4551
const deprecatedRegion = getOption<string>(options, ['region', 'r', 'R']);
@@ -62,11 +68,18 @@ export default async function mk(options: Record<string, unknown>) {
6268
);
6369
}
6470

71+
if (sourceSnapshot && !forkOf) {
72+
console.error('--source-snapshot requires --fork-of');
73+
process.exit(1);
74+
}
75+
6576
const { error } = await createBucket(bucket, {
6677
defaultTier: (defaultTier ?? 'STANDARD') as StorageClass,
6778
enableSnapshot: enableSnapshots === true,
6879
access: (access ?? 'private') as 'public' | 'private',
6980
locations: parseLocations(locations ?? 'global'),
81+
...(forkOf ? { sourceBucketName: forkOf } : {}),
82+
...(sourceSnapshot ? { sourceBucketSnapshot: sourceSnapshot } : {}),
7083
config,
7184
});
7285

src/specs.yaml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ commands:
262262
- "tigris mk my-bucket --access public --region iad"
263263
- "tigris mk my-bucket/images/"
264264
- "tigris mk t3://my-bucket"
265+
- "tigris mk my-fork --fork-of my-bucket"
266+
- "tigris mk my-fork --fork-of my-bucket --source-snapshot 1765889000501544464"
265267
messages:
266268
onFailure: 'Failed to create. Check the name is valid and your credentials have write access'
267269
arguments:
@@ -307,6 +309,12 @@ commands:
307309
alias: l
308310
options: *location_options
309311
default: 'global'
312+
- name: fork-of
313+
description: Create this bucket as a fork (copy-on-write clone) of the named source bucket
314+
alias: fork
315+
- name: source-snapshot
316+
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
317+
alias: source-snap
310318

311319
# touch
312320
- name: touch
@@ -578,6 +586,7 @@ commands:
578586
examples:
579587
- "tigris buckets list"
580588
- "tigris buckets list --format json"
589+
- "tigris buckets list --forks-of my-bucket"
581590
messages:
582591
onStart: 'Listing buckets...'
583592
onSuccess: 'Found {{count}} bucket(s)'
@@ -589,6 +598,8 @@ commands:
589598
alias: f
590599
options: [json, table, xml]
591600
default: table
601+
- name: forks-of
602+
description: Only list buckets that are forks of the named source bucket
592603
# create
593604
- name: create
594605
description: Create a new bucket with optional access, tier, and location settings
@@ -597,6 +608,8 @@ commands:
597608
- "tigris buckets create my-bucket"
598609
- "tigris buckets create my-bucket --access public --locations iad"
599610
- "tigris buckets create my-bucket --enable-snapshots --default-tier STANDARD_IA"
611+
- "tigris buckets create my-fork --fork-of my-bucket"
612+
- "tigris buckets create my-fork --fork-of my-bucket --source-snapshot 1765889000501544464"
600613
messages:
601614
onStart: 'Creating bucket...'
602615
onSuccess: "Bucket '{{name}}' created successfully"
@@ -641,6 +654,12 @@ commands:
641654
alias: l
642655
options: *location_options
643656
default: 'global'
657+
- name: fork-of
658+
description: Create this bucket as a fork (copy-on-write clone) of the named source bucket
659+
alias: fork
660+
- name: source-snapshot
661+
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
662+
alias: source-snap
644663
# get
645664
- name: get
646665
description: Show details for a bucket including access level, region, tier, and custom domain
@@ -935,15 +954,16 @@ commands:
935954
# Manage forks
936955
#########################
937956
- name: forks
938-
description: List and create forks. A fork is a writable copy-on-write clone of a bucket, useful for testing or branching data
957+
description: (Deprecated, use "buckets create --fork-of" and "buckets list --forks-of") List and create forks
939958
alias: f
940959
examples:
941960
- "tigris forks list my-bucket"
942961
- "tigris forks create my-bucket my-fork"
943962
commands:
944963
# list
945964
- name: list
946-
description: List all forks created from the given source bucket
965+
description: (Deprecated, use "buckets list --forks-of") List all forks created from the given source bucket
966+
deprecated: true
947967
alias: l
948968
examples:
949969
- "tigris forks list my-bucket"
@@ -953,6 +973,7 @@ commands:
953973
onSuccess: 'Found {{count}} fork(s)'
954974
onFailure: 'Failed to list forks'
955975
onEmpty: 'No forks found for this bucket'
976+
onDeprecated: 'Use "tigris buckets list --forks-of <bucket>" instead'
956977
arguments:
957978
- name: name
958979
description: Name of the source bucket
@@ -967,15 +988,17 @@ commands:
967988
default: table
968989
# create
969990
- name: create
970-
description: Create a new fork (copy-on-write clone) of the source bucket. Optionally fork from a specific snapshot
991+
description: (Deprecated, use "buckets create --fork-of") Create a new fork (copy-on-write clone) of the source bucket
992+
deprecated: true
971993
alias: c
972994
examples:
973995
- "tigris forks create my-bucket my-fork"
974-
- "tigris forks create my-bucket my-fork --snapshot snap-2025-01-01"
996+
- "tigris forks create my-bucket my-fork --snapshot 1765889000501544464"
975997
messages:
976998
onStart: 'Creating fork...'
977999
onSuccess: "Fork '{{forkName}}' created from '{{name}}'"
9781000
onFailure: 'Failed to create fork'
1001+
onDeprecated: 'Use "tigris buckets create <name> --fork-of <bucket>" instead'
9791002
arguments:
9801003
- name: name
9811004
description: Name of the source bucket
@@ -990,7 +1013,7 @@ commands:
9901013
examples:
9911014
- my-fork
9921015
- name: snapshot
993-
description: Create fork from a specific snapshot
1016+
description: Create fork from a specific snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464)
9941017
alias: s
9951018

9961019
#########################

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Messages {
1919
onFailure?: string;
2020
onEmpty?: string;
2121
onAlreadyDone?: string;
22+
onDeprecated?: string;
2223
hint?: string;
2324
}
2425

@@ -31,6 +32,7 @@ export interface CommandSpec {
3132
examples?: string[];
3233
commands?: CommandSpec[]; // recursive - can nest infinitely
3334
default?: string;
35+
deprecated?: boolean;
3436
message?: string;
3537
messages?: Messages;
3638
}

src/utils/messages.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ export function printHint(
150150
}
151151
}
152152

153+
/**
154+
* Print a deprecation warning for a command
155+
* Suppressed when output is piped/redirected
156+
*/
157+
export function printDeprecated(message: string): void {
158+
if (!isTTY()) return;
159+
console.warn(`⚠ Deprecated: ${message}`);
160+
}
161+
153162
/**
154163
* Helper to create a message context
155164
*/

0 commit comments

Comments
 (0)