Skip to content

Commit 26dfed6

Browse files
designcodeclaude
andauthored
feat!: lifecycle commands; remove deprecated surface (#95)
* feat!: add buckets lifecycle commands; remove deprecated surface Add tigris buckets lifecycle (alias lc) with list, create, edit for multi-rule lifecycle management (transitions and TTL/expiration with optional prefix filter), backed by the updated @tigrisdata/storage SDK. Add a generic removed/replaced_by mechanism on CommandSpec and Argument so the cli-core can intercept removed entries with a clean redirect message instead of "unknown command/option". Removed commands are hidden from help and have no implementation file. Breaking changes: - buckets set-ttl and buckets set-transition removed; use buckets lifecycle create/edit - forks list and forks create removed; use buckets create --fork-of and buckets list --forks-of - --region and --consistency flags on mk, buckets create, buckets set removed; use --locations Other cleanup: - drop OperationSpec alias, ParsedPaths type, unused YAML anchors - update-docs and generate-registry skip removed entries - bucket-info no longer renders the deprecated ttlConfig row; the TTL rule appears under Lifecycle Rules like any other rule Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5589232 commit 26dfed6

28 files changed

Lines changed: 1087 additions & 942 deletions

package-lock.json

Lines changed: 120 additions & 120 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"scripts": {
2525
"build": "tsc --noEmit && tsup",
2626
"dev": "export $(grep -v '^#' .env.test | xargs) && (tsc --noEmit --watch --preserveWatchOutput & tsup --watch)",
27-
"cli": "node dist/cli.js",
27+
"cli": "export $(grep -v '^#' .env.test | xargs) && node dist/cli.js",
2828
"lint": "eslint src test",
2929
"lint:fix": "eslint src test --fix",
3030
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@@ -93,7 +93,7 @@
9393
"@aws-sdk/credential-providers": "^3.1038.0",
9494
"@smithy/shared-ini-file-loader": "^4.4.9",
9595
"@tigrisdata/iam": "^2.1.1",
96-
"@tigrisdata/storage": "^3.2.1",
96+
"@tigrisdata/storage": "^3.4.0",
9797
"commander": "^14.0.3",
9898
"enquirer": "^2.4.1",
9999
"jose": "^6.2.3",

scripts/generate-registry.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,12 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
1313
import { join } from 'path';
1414
import * as YAML from 'yaml';
1515

16+
import type { CommandSpec, Specs } from '../src/types.js';
17+
1618
const ROOT = process.cwd();
1719
const SPECS_PATH = join(ROOT, 'src/specs.yaml');
1820
const OUTPUT_PATH = join(ROOT, 'src/command-registry.ts');
1921

20-
interface CommandSpec {
21-
name: string;
22-
alias?: string;
23-
commands?: CommandSpec[];
24-
default?: string;
25-
}
26-
27-
interface Specs {
28-
commands: CommandSpec[];
29-
}
30-
3122
interface RegistryEntry {
3223
key: string;
3324
importName: string;
@@ -88,6 +79,10 @@ function collectEntries(
8879
const entries: RegistryEntry[] = [];
8980

9081
for (const cmd of commands) {
82+
// Removed commands have no implementation file by design — the
83+
// cli-core intercepts them and prints a redirect message.
84+
if (cmd.removed) continue;
85+
9186
const currentPath = [...parentPath, cmd.name];
9287

9388
if (cmd.commands && cmd.commands.length > 0) {

scripts/update-docs.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ function isImplemented(...parts: string[]): boolean {
2323
return paths.some((p) => existsSync(p) && !p.includes('/_'));
2424
}
2525

26-
// Check if a command or any of its nested subcommands are implemented
26+
// Check if a command or any of its nested subcommands are implemented.
27+
// Removed commands are tombstones and never count as "implemented" for
28+
// docs purposes — they shouldn't appear in the rendered README.
2729
function hasImplementation(cmd: CommandSpec, ...parentParts: string[]): boolean {
30+
if (cmd.removed) return false;
2831
const parts = [...parentParts, cmd.name];
2932
if (isImplemented(...parts)) return true;
3033
if (cmd.commands) {
@@ -49,7 +52,7 @@ function generateCommandSection(cmd: CommandSpec): string {
4952

5053
lines.push(`### \`${cmd.name}\`${aliasStr}`);
5154
lines.push('');
52-
lines.push(cmd.description);
55+
lines.push(cmd.description ?? '');
5356
lines.push('');
5457
lines.push('```');
5558
const usage = getCommandUsage(cmd);
@@ -58,7 +61,8 @@ function generateCommandSection(cmd: CommandSpec): string {
5861
lines.push('```');
5962
lines.push('');
6063

61-
const flags = cmd.arguments?.filter((a) => a.type !== 'positional') || [];
64+
const flags =
65+
cmd.arguments?.filter((a) => a.type !== 'positional' && !a.removed) || [];
6266
if (flags.length > 0) {
6367
lines.push('| Flag | Description |');
6468
lines.push('|------|-------------|');
@@ -113,7 +117,7 @@ function generateResourceSection(
113117

114118
lines.push(`${headerLevel} \`${fullName}\`${aliasStr}`);
115119
lines.push('');
116-
lines.push(cmd.description);
120+
lines.push(cmd.description ?? '');
117121
lines.push('');
118122

119123
const subcommands = cmd.commands || [];
@@ -270,7 +274,7 @@ function generateDocs(specs: Specs): string {
270274
}
271275

272276
// Resource management
273-
const resourceCommands = ['organizations', 'access-keys', 'credentials', 'buckets', 'forks', 'snapshots', 'objects', 'iam'];
277+
const resourceCommands = ['organizations', 'access-keys', 'credentials', 'buckets', 'snapshots', 'objects', 'iam'];
274278
const implementedResources = resourceCommands.filter((c) => {
275279
const cmd = specs.commands.find((s) => s.name === c);
276280
if (!cmd) return false;
@@ -341,12 +345,12 @@ function generateDocs(specs: Specs): string {
341345
}
342346
}
343347

344-
// Buckets section (buckets, forks, snapshots)
345-
const bucketRelated = ['buckets', 'forks', 'snapshots'].filter((c) => implementedResources.includes(c));
348+
// Buckets section (buckets, snapshots)
349+
const bucketRelated = ['buckets', 'snapshots'].filter((c) => implementedResources.includes(c));
346350
if (bucketRelated.length > 0) {
347351
lines.push('### Buckets');
348352
lines.push('');
349-
lines.push('Buckets are containers for objects. You can also create forks and snapshots of buckets.');
353+
lines.push('Buckets are containers for objects. You can also create snapshots of buckets.');
350354
lines.push('');
351355

352356
for (const cmdName of bucketRelated) {

src/auth/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ export interface Auth0Config {
2525
export function getAuth0Config(): Auth0Config {
2626
const isDev = process.env.TIGRIS_ENV === 'development';
2727
const domain = isDev
28-
? 'auth-dev.tigris.dev'
28+
? (process.env.AUTH0_DOMAIN ?? 'auth-storage.tigris.dev')
2929
: (process.env.AUTH0_DOMAIN ?? 'auth.storage.tigrisdata.io');
3030
const clientId = isDev
31-
? 'JdJVYIyw0O1uHi5L5OJH903qaWBgd3gF'
31+
? (process.env.AUTH0_CLIENT_ID ?? 'JdJVYIyw0O1uHi5L5OJH903qaWBgd3gF')
3232
: (process.env.AUTH0_CLIENT_ID ?? 'DMejqeM3CQ4IqTjEcd3oA9eEiT40hn8D');
3333
const audience = isDev
34-
? 'https://tigris-api-dev'
34+
? (process.env.AUTH0_AUDIENCE ?? 'https://tigris-api-dev')
3535
: (process.env.AUTH0_AUDIENCE ?? 'https://tigris-os-api');
3636
const claimsNamespace =
3737
process.env.TIGRIS_CLAIMS_NAMESPACE ?? 'https://tigris';

src/cli-core.ts

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { exitWithError } from '@utils/exit.js';
66
import { printDeprecated } from '@utils/messages.js';
7-
import { Command as CommanderCommand } from 'commander';
7+
import { Command as CommanderCommand, Option } from 'commander';
88

99
import type { Argument, CommandSpec, Specs } from './types.js';
1010

@@ -164,6 +164,12 @@ export function commandHasAnyImplementation(
164164
pathParts: string[],
165165
hasImplementation: ImplementationChecker
166166
): boolean {
167+
// Removed commands are still registered so we can intercept and
168+
// redirect users to the replacement instead of "unknown command".
169+
if (command.removed) {
170+
return true;
171+
}
172+
167173
if (hasImplementation(pathParts)) {
168174
return true;
169175
}
@@ -181,22 +187,58 @@ export function commandHasAnyImplementation(
181187
return false;
182188
}
183189

190+
/**
191+
* Print a redirect message and exit. Used for hard-removed commands
192+
* and arguments. `subject` is the human-readable thing the user invoked
193+
* (e.g. `tigris buckets set-ttl` or `--region`).
194+
*/
195+
function printRemovedAndExit(
196+
subject: string,
197+
replacedBy: string | undefined
198+
): never {
199+
const hint = replacedBy
200+
? ` Use ${replacedBy} instead.`
201+
: ' See the changelog for migration guidance.';
202+
console.error(`${subject} was removed in this version.${hint}`);
203+
process.exit(1);
204+
}
205+
206+
/**
207+
* Inspect parsed options for any argument the spec marks as removed.
208+
* If the user supplied one, print the redirect and exit.
209+
*/
210+
function checkRemovedArguments(
211+
args: Argument[] | undefined,
212+
options: Record<string, unknown>
213+
): void {
214+
if (!args) return;
215+
for (const arg of args) {
216+
if (!arg.removed) continue;
217+
const value = getOptionValue(options, arg.name, args);
218+
if (value !== undefined) {
219+
printRemovedAndExit(`--${arg.name}`, arg.replaced_by);
220+
}
221+
}
222+
}
223+
184224
export function showCommandHelp(
185225
specs: Specs,
186226
command: CommandSpec,
187227
pathParts: string[],
188228
hasImplementation: ImplementationChecker
189229
) {
190230
const fullPath = pathParts.join(' ');
191-
console.log(`\n${specs.name} ${fullPath} - ${command.description}\n`);
231+
console.log(`\n${specs.name} ${fullPath} - ${command.description ?? ''}\n`);
192232

193233
if (command.commands && command.commands.length > 0) {
194-
const availableCmds = command.commands.filter((cmd) =>
195-
commandHasAnyImplementation(
196-
cmd,
197-
[...pathParts, cmd.name],
198-
hasImplementation
199-
)
234+
const availableCmds = command.commands.filter(
235+
(cmd) =>
236+
!cmd.removed &&
237+
commandHasAnyImplementation(
238+
cmd,
239+
[...pathParts, cmd.name],
240+
hasImplementation
241+
)
200242
);
201243

202244
if (availableCmds.length > 0) {
@@ -208,14 +250,17 @@ export function showCommandHelp(
208250
cmdPart += ` (${aliases.join(', ')})`;
209251
}
210252
const paddedCmdPart = cmdPart.padEnd(24);
211-
console.log(`${paddedCmdPart}${cmd.description}`);
253+
console.log(`${paddedCmdPart}${cmd.description ?? ''}`);
212254
});
213255
console.log();
214256
}
215257
}
216258

217259
const globalArgs = specs.definitions?.global_arguments ?? [];
218-
const effectiveArgs = getEffectiveArguments(globalArgs, command.arguments);
260+
const effectiveArgs = getEffectiveArguments(
261+
globalArgs,
262+
command.arguments
263+
).filter((arg) => !arg.removed);
219264
if (effectiveArgs.length > 0) {
220265
console.log('Arguments:');
221266
effectiveArgs.forEach((arg) => {
@@ -248,8 +293,10 @@ export function showMainHelp(
248293
console.log('Usage: tigris [command] [options]\n');
249294
console.log('Commands:');
250295

251-
const availableCommands = specs.commands.filter((cmd) =>
252-
commandHasAnyImplementation(cmd, [cmd.name], hasImplementation)
296+
const availableCommands = specs.commands.filter(
297+
(cmd) =>
298+
!cmd.removed &&
299+
commandHasAnyImplementation(cmd, [cmd.name], hasImplementation)
253300
);
254301

255302
availableCommands.forEach((command: CommandSpec) => {
@@ -261,7 +308,7 @@ export function showMainHelp(
261308
commandPart += ` (${aliases.join(', ')})`;
262309
}
263310
const paddedCommandPart = commandPart.padEnd(24);
264-
console.log(`${paddedCommandPart}${command.description}`);
311+
console.log(`${paddedCommandPart}${command.description ?? ''}`);
265312
});
266313
console.log(
267314
`\nUse "${specs.name} <command> help" for more information about a command.`
@@ -319,7 +366,15 @@ export function addArgumentsToCommand(
319366
arg.required || arg['required-when'] ? ' <value>' : ' [value]';
320367
}
321368

322-
cmd.option(optionString, arg.description, arg.default);
369+
if (arg.removed) {
370+
// Register but hide from --help so commander still parses the
371+
// value; the dispatch handler intercepts it post-parse.
372+
cmd.addOption(
373+
new Option(optionString, arg.description ?? '').hideHelp()
374+
);
375+
} else {
376+
cmd.option(optionString, arg.description ?? '', arg.default);
377+
}
323378
}
324379
});
325380
}
@@ -493,13 +548,29 @@ export function registerCommands(
493548
continue;
494549
}
495550

496-
const cmd = parent.command(spec.name).description(spec.description);
551+
const cmd = parent
552+
.command(spec.name, spec.removed ? { hidden: true } : undefined)
553+
.description(spec.description ?? '');
497554

498555
if (spec.alias) {
499556
const aliases = Array.isArray(spec.alias) ? spec.alias : [spec.alias];
500557
aliases.forEach((alias) => cmd.alias(alias));
501558
}
502559

560+
// Removed commands: register a redirect-and-exit action; skip
561+
// children, arguments, and help registration entirely.
562+
if (spec.removed) {
563+
cmd.allowUnknownOption(true);
564+
cmd.allowExcessArguments(true);
565+
cmd.action(() => {
566+
printRemovedAndExit(
567+
`${specs.name} ${currentPath.join(' ')}`,
568+
spec.replaced_by
569+
);
570+
});
571+
continue;
572+
}
573+
503574
if (spec.commands && spec.commands.length > 0) {
504575
// Has children - recurse
505576
registerCommands(config, cmd, spec.commands, currentPath);
@@ -524,16 +595,21 @@ export function registerCommands(
524595
hasImplementation
525596
);
526597

598+
const extracted = extractArgumentValues(
599+
allArguments,
600+
positionalArgs,
601+
options
602+
);
603+
527604
if (
528605
allArguments.length > 0 &&
529-
!validateRequiredWhen(
530-
allArguments,
531-
extractArgumentValues(allArguments, positionalArgs, options)
532-
)
606+
!validateRequiredWhen(allArguments, extracted)
533607
) {
534608
return;
535609
}
536610

611+
checkRemovedArguments(allArguments, extracted);
612+
537613
if (defaultCmd.deprecated && defaultCmd.messages?.onDeprecated) {
538614
printDeprecated(defaultCmd.messages.onDeprecated);
539615
}
@@ -542,7 +618,7 @@ export function registerCommands(
542618
loadModule,
543619
[...currentPath, defaultCmd.name],
544620
positionalArgs,
545-
extractArgumentValues(allArguments, positionalArgs, options)
621+
extracted
546622
);
547623
});
548624
}
@@ -570,16 +646,21 @@ export function registerCommands(
570646
const options = args.pop();
571647
const positionalArgs = args;
572648

649+
const extracted = extractArgumentValues(
650+
spec.arguments || [],
651+
positionalArgs,
652+
options
653+
);
654+
573655
if (
574656
spec.arguments &&
575-
!validateRequiredWhen(
576-
spec.arguments,
577-
extractArgumentValues(spec.arguments, positionalArgs, options)
578-
)
657+
!validateRequiredWhen(spec.arguments, extracted)
579658
) {
580659
return;
581660
}
582661

662+
checkRemovedArguments(spec.arguments, extracted);
663+
583664
if (spec.deprecated && spec.messages?.onDeprecated) {
584665
printDeprecated(spec.messages.onDeprecated);
585666
}
@@ -588,7 +669,7 @@ export function registerCommands(
588669
loadModule,
589670
currentPath,
590671
positionalArgs,
591-
extractArgumentValues(spec.arguments || [], positionalArgs, options)
672+
extracted
592673
);
593674
});
594675
}

0 commit comments

Comments
 (0)