Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 137 additions & 26 deletions cli/src/commands/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,30 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
if (typeof cacheConfig === 'object' && cacheConfig !== null) {
const config = cacheConfig as Record<string, any>;
Console.info(chalk.bold.blue('Enabled: ') + chalk.white(config.enabled ? 'Yes' : 'No'));
Console.info(chalk.bold.blue('TTL: ') + chalk.white(config.ttl || 'N/A'));
Console.info(chalk.bold.blue('Max Size: ') + chalk.white(config.max_size || 'N/A'));
Console.info(chalk.bold.blue('Strategy: ') + chalk.white(config.strategy || 'N/A'));
Console.info(chalk.bold.blue('Table: ') + chalk.white(config.table || 'N/A'));
Console.info(chalk.bold.blue('Schema: ') + chalk.white(config.schema || 'N/A'));
Console.info(chalk.bold.blue('Schedule: ') + chalk.white(config.schedule || 'N/A'));
if (Array.isArray(config['primary-key'])) {
Console.info(chalk.bold.blue('Primary Key: ') + chalk.white(config['primary-key'].join(', ')));
}
if (config.cursor) {
Console.info(
chalk.bold.blue('Cursor: ') +
chalk.white(`${config.cursor.column || 'N/A'} (${config.cursor.type || 'unknown'})`)
);
}
if (config['rollback-window']) {
Console.info(chalk.bold.blue('Rollback Window: ') + chalk.white(config['rollback-window']));
}
if (config.retention) {
Console.info(chalk.bold.blue('Retention: ') + chalk.white(JSON.stringify(config.retention)));
}
if (config['delete-handling']) {
Console.info(chalk.bold.blue('Delete Handling: ') + chalk.white(config['delete-handling']));
}
if (config['template-file']) {
Console.info(chalk.bold.blue('Template File: ') + chalk.white(config['template-file']));
}
} else {
Console.info(chalk.gray('No cache configuration found'));
}
Expand All @@ -89,16 +110,32 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
.command('update <path>')
.description('Update cache configuration')
.option('-e, --enabled <enabled>', 'Enable/disable caching (true/false)')
.option('-t, --ttl <ttl>', 'Cache TTL in seconds')
.option('-s, --max-size <size>', 'Maximum cache size')
.option('--strategy <strategy>', 'Cache strategy (lru, ttl, etc.)')
.option('--table <table>', 'Cache table name')
.option('--schema <schema>', 'Cache schema name')
.option('--schedule <cron>', 'Cache refresh schedule or interval string')
.option('--primary-key <columns...>', 'Primary key columns (space separated)')
.option('--cursor-column <column>', 'Cursor column name')
.option('--cursor-type <type>', 'Cursor data type')
.option('--rollback-window <duration>', 'Rollback window (e.g. 6h)')
.option('--retention-keep <count>', 'Number of snapshots to keep')
.option('--retention-age <duration>', 'Max snapshot age (e.g. 7d)')
.option('--delete-handling <mode>', 'Delete handling strategy')
.option('--template-file <path>', 'Cache template SQL file')
.option('-f, --file <file>', 'JSON file containing cache configuration')
.option('--stdin', 'Read cache configuration from stdin')
.action(async (path: string, options: {
enabled?: string;
ttl?: string;
maxSize?: string;
strategy?: string;
table?: string;
schema?: string;
schedule?: string;
primaryKey?: string[];
cursorColumn?: string;
cursorType?: string;
rollbackWindow?: string;
retentionKeep?: string;
retentionAge?: string;
deleteHandling?: string;
templateFile?: string;
file?: string;
stdin?: boolean;
}) => {
Expand Down Expand Up @@ -138,14 +175,46 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
if (options.enabled !== undefined) {
cacheConfig.enabled = options.enabled === 'true';
}
if (options.ttl !== undefined) {
cacheConfig.ttl = parseInt(options.ttl);
if (options.table) {
cacheConfig.table = options.table;
}
if (options.schema) {
cacheConfig.schema = options.schema;
}
if (options.schedule) {
cacheConfig.schedule = options.schedule;
}
if (options.primaryKey && options.primaryKey.length > 0) {
cacheConfig['primary-key'] = options.primaryKey;
}
if ((options.cursorColumn && !options.cursorType) || (!options.cursorColumn && options.cursorType)) {
Console.error('Both --cursor-column and --cursor-type must be provided together');
process.exitCode = 1;
return;
}
if (options.cursorColumn && options.cursorType) {
cacheConfig.cursor = {
column: options.cursorColumn,
type: options.cursorType,
};
}
if (options.rollbackWindow) {
cacheConfig['rollback-window'] = options.rollbackWindow;
}
if (options.retentionKeep || options.retentionAge) {
cacheConfig.retention = {};
if (options.retentionKeep) {
cacheConfig.retention['keep-last-snapshots'] = Number(options.retentionKeep);
}
if (options.retentionAge) {
cacheConfig.retention['max-snapshot-age'] = options.retentionAge;
}
}
if (options.maxSize !== undefined) {
cacheConfig.max_size = parseInt(options.maxSize);
if (options.deleteHandling) {
cacheConfig['delete-handling'] = options.deleteHandling;
}
if (options.strategy !== undefined) {
cacheConfig.strategy = options.strategy;
if (options.templateFile) {
cacheConfig['template-file'] = options.templateFile;
}

if (Object.keys(cacheConfig).length === 0) {
Expand Down Expand Up @@ -176,14 +245,14 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
const endpointUrl = buildEndpointUrl(path, 'cache/template');
const response = await ctx.client.get(endpointUrl);
spinner.succeed(chalk.green(`✓ Cache template for ${path} retrieved`));
const template = response.data;
const template = typeof response.data === 'string' ? response.data : response.data?.template;

if (ctx.config.output === 'json') {
renderJson(template, ctx.config.jsonStyle);
renderJson({ template: template ?? '' }, ctx.config.jsonStyle);
} else {
Console.info(chalk.cyan(`\n📄 Cache Template: ${path}`));
Console.info(chalk.gray('═'.repeat(60)));
Console.info(chalk.white(template));
Console.info(chalk.white(template ?? ''));
}
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to fetch cache template for ${path}`));
Expand Down Expand Up @@ -256,23 +325,65 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
const result = response.data;

if (ctx.config.output === 'json') {
renderJson(result, ctx.config.jsonStyle);
renderJson(result || { success: true }, ctx.config.jsonStyle);
} else {
Console.info(chalk.cyan(`\n🔄 Cache Refresh: ${path}`));
Console.info(chalk.gray('═'.repeat(60)));
Console.info(chalk.green('✓ Cache refreshed successfully'));

if (result.entries_cleared !== undefined) {
Console.info(chalk.blue(`Entries cleared: ${result.entries_cleared}`));
}
if (result.cache_size !== undefined) {
Console.info(chalk.blue(`New cache size: ${result.cache_size}`));
}
}
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to refresh cache for ${path}`));
handleError(error, ctx.config);
process.exitCode = 1;
}
});

cache
.command('gc <path>')
.description('Run DuckLake garbage collection for endpoint cache')
.action(async (path: string) => {
const spinner = Console.spinner(`Running cache GC for ${path}...`);
try {
const endpointUrl = buildEndpointUrl(path, 'cache/gc');
await ctx.client.post(endpointUrl);
spinner.succeed(chalk.green(`✓ Cache GC triggered for ${path}`));
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to run cache GC for ${path}`));
handleError(error, ctx.config);
process.exitCode = 1;
}
});

cache
.command('audit <path>')
.description('Show DuckLake audit log entries for an endpoint')
.action(async (path: string) => {
const spinner = Console.spinner(`Fetching cache audit for ${path}...`);
try {
const endpointUrl = buildEndpointUrl(path, 'cache/audit');
const response = await ctx.client.get(endpointUrl);
spinner.succeed(chalk.green(`✓ Cache audit for ${path} retrieved`));
renderJson(response.data, ctx.config.jsonStyle);
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to fetch cache audit for ${path}`));
handleError(error, ctx.config);
process.exitCode = 1;
}
});

cache
.command('audit-all')
.description('Show DuckLake audit log across all caches')
.action(async () => {
const spinner = Console.spinner('Fetching cache audit log...');
try {
const response = await ctx.client.get('/api/v1/_config/cache/audit');
spinner.succeed(chalk.green('✓ Cache audit log retrieved'));
renderJson(response.data, ctx.config.jsonStyle);
} catch (error) {
spinner.fail(chalk.red('✗ Failed to fetch cache audit log'));
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
47 changes: 47 additions & 0 deletions cli/src/commands/config/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Command } from 'commander';
import type { CliContext } from '../../lib/types';
import { Console } from '../../lib/console';
import { handleError } from '../../lib/errors';
import { renderJson } from '../../lib/render';
import chalk from 'chalk';

export function registerEnvironmentCommand(config: Command, ctx: CliContext) {
config
.command('env')
.description('Show environment variables exposed by the server')
.action(async () => {
const spinner = Console.spinner('Fetching environment variables...');
try {
const response = await ctx.client.get('/api/v1/_config/environment-variables');
spinner.succeed(chalk.green('✓ Environment variables retrieved'));
const payload = response.data;

if (ctx.config.output === 'json') {
renderJson(payload, ctx.config.jsonStyle);
return;
}

Console.info(chalk.cyan('\n🌿 Environment Variables'));
Console.info(chalk.gray('═'.repeat(60)));

const variables = Array.isArray(payload?.variables) ? payload.variables : [];
if (variables.length === 0) {
Console.info(chalk.gray('No environment variables are configured.'));
return;
}

variables.forEach((variable: any) => {
const available = variable.available ? chalk.green('available') : chalk.red('missing');
const value = variable.value ?? '';
Console.info(`${chalk.bold(variable.name)} - ${available}`);
if (value) {
Console.info(chalk.gray(` Value: ${value}`));
}
});
} catch (error) {
spinner.fail(chalk.red('✗ Failed to fetch environment variables'));
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
75 changes: 75 additions & 0 deletions cli/src/commands/config/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Command } from 'commander';
import type { CliContext } from '../../lib/types';
import { Console } from '../../lib/console';
import { handleError } from '../../lib/errors';
import { renderJson } from '../../lib/render';
import chalk from 'chalk';

interface FilesystemNode {
name: string;
type: 'file' | 'directory';
path: string;
children?: FilesystemNode[];
extension?: string;
yaml_type?: string;
}

function renderTree(nodes: FilesystemNode[], prefix = '') {
nodes.forEach((node, index) => {
const isLast = index === nodes.length - 1;
const branch = isLast ? '└─' : '├─';
const nextPrefix = prefix + (isLast ? ' ' : '│ ');

const label = node.type === 'directory'
? chalk.blue(`[dir] ${node.name}`)
: chalk.white(node.name);

Console.info(`${prefix}${branch} ${label}`);

if (node.type === 'file' && node.yaml_type) {
Console.info(`${nextPrefix}${chalk.gray(`(${node.yaml_type})`)}`);
}

if (node.children && node.children.length > 0) {
renderTree(node.children, nextPrefix);
}
});
}

export function registerFilesystemCommand(config: Command, ctx: CliContext) {
config
.command('filesystem')
.description('Inspect server-side filesystem tree for templates')
.action(async () => {
const spinner = Console.spinner('Fetching filesystem structure...');
try {
const response = await ctx.client.get('/api/v1/_config/filesystem');
spinner.succeed(chalk.green('✓ Filesystem data retrieved'));
const payload = response.data;

if (ctx.config.output === 'json') {
renderJson(payload, ctx.config.jsonStyle);
return;
}

Console.info(chalk.cyan('\n📁 flapi Filesystem'));
Console.info(chalk.gray('═'.repeat(60)));
Console.info(`${chalk.bold('Base Path:')} ${payload?.base_path || 'N/A'}`);
Console.info(`${chalk.bold('Templates Path:')} ${payload?.template_path || 'N/A'}`);
if (payload?.config_file) {
Console.info(`${chalk.bold('Config File:')} ${payload.config_file} (${payload?.config_file_exists ? 'found' : 'missing'})`);
}
Console.info('');

if (Array.isArray(payload?.tree) && payload.tree.length > 0) {
renderTree(payload.tree);
} else {
Console.info(chalk.gray('No files discovered under the template path.'));
}
} catch (error) {
spinner.fail(chalk.red('✗ Failed to fetch filesystem data'));
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
5 changes: 4 additions & 1 deletion cli/src/commands/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import type { CliContext } from '../../lib/types';
import { registerConfigCommand } from './show';
import { registerValidateCommand } from './validate';
import { registerLogLevelCommands } from './log-level';
import { registerEnvironmentCommand } from './environment';
import { registerFilesystemCommand } from './filesystem';

export function registerConfigCommands(program: Command, ctx: CliContext) {
const configCmd = registerConfigCommand(program, ctx);
registerValidateCommand(configCmd, ctx);
registerLogLevelCommands(configCmd, ctx);
registerEnvironmentCommand(configCmd, ctx);
registerFilesystemCommand(configCmd, ctx);
}

Loading