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
3 changes: 2 additions & 1 deletion cli/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
"prepare": "npm run build"
},
"dependencies": {
"axios": "^1.7.7",
Expand Down
6 changes: 4 additions & 2 deletions cli/shared/src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,14 @@ export class FlapiApiClient {
}

/**
* Test an endpoint with given parameters
* Test an endpoint with given parameters.
* Targets the server's `/template/test` route (the only test route that exists;
* see src/config_service.cpp).
*/
async testEndpoint(pathOrName: string, parameters: Record<string, any>): Promise<any> {
const slug = pathToSlug(pathOrName);
const response = await this.client.post(
`/api/v1/_config/endpoints/${slug}/test`,
`/api/v1/_config/endpoints/${slug}/template/test`,
{ parameters }
);
return response.data;
Expand Down
47 changes: 47 additions & 0 deletions cli/src/commands/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,51 @@ export function registerCacheCommands(program: Command, ctx: CliContext) {
process.exitCode = 1;
}
});

cache
.command('gc <path>')
.description('Run garbage collection on cache snapshots (DuckLake)')
.action(async (path: string) => {
const spinner = Console.spinner(`Running cache GC for ${path}...`);
try {
const endpointUrl = buildEndpointUrl(path, 'cache/gc');
const response = await ctx.client.post(endpointUrl, {});
spinner.succeed(chalk.green(`✓ Cache GC for ${path} completed`));

if (ctx.config.output !== 'json') {
Console.info(chalk.cyan(`\n🧹 Cache GC: ${path}`));
Console.info(chalk.gray('═'.repeat(60)));
}
renderJson(response.data, ctx.config.jsonStyle);
} 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('Get cache sync audit log (for one endpoint, or all endpoints if no path given)')
.action(async (path?: string) => {
const target = path ? `for ${path}` : '(all endpoints)';
const spinner = Console.spinner(`Fetching cache audit log ${target}...`);
try {
const url = path
? buildEndpointUrl(path, 'cache/audit')
: '/api/v1/_config/cache/audit';
const response = await ctx.client.get(url);
spinner.succeed(chalk.green(`✓ Cache audit log ${target} retrieved`));

if (ctx.config.output !== 'json') {
Console.info(chalk.cyan(`\n📋 Cache Audit ${target}`));
Console.info(chalk.gray('═'.repeat(60)));
}
renderJson(response.data, ctx.config.jsonStyle);
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to fetch cache audit log ${target}`));
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
2 changes: 2 additions & 0 deletions cli/src/commands/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type { CliContext } from '../../lib/types';
import { registerConfigCommand } from './show';
import { registerValidateCommand } from './validate';
import { registerLogLevelCommands } from './log-level';
import { registerInfoCommands } from './info';

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

76 changes: 76 additions & 0 deletions cli/src/commands/config/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 registerInfoCommands(config: Command, ctx: CliContext) {
config
.command('env')
.description('List whitelisted environment variables and their availability')
.action(async () => {
let spinner;
if (ctx.config.output !== 'json') {
spinner = Console.spinner('Fetching environment variables...');
}
try {
const response = await ctx.client.get('/api/v1/_config/environment-variables');
if (spinner) {
spinner.succeed(chalk.green('✓ Environment variables retrieved'));
}
const data = response.data;

if (ctx.config.output === 'json') {
renderJson(data, ctx.config.jsonStyle);
} else {
Console.info(chalk.cyan('\n🔐 Environment Variables'));
Console.info(chalk.gray('═'.repeat(60)));
const vars = Array.isArray(data?.variables) ? data.variables : [];
if (vars.length === 0) {
Console.info(chalk.gray('No environment variables configured'));
} else {
vars.forEach((v: any) => {
const status = v.available ? chalk.green('available') : chalk.yellow('not set');
Console.info(chalk.bold.blue(v.name) + ' ' + status);
});
}
}
} catch (error) {
if (spinner) {
spinner.fail(chalk.red('✗ Failed to fetch environment variables'));
}
handleError(error, ctx.config);
process.exitCode = 1;
}
});

config
.command('filesystem')
.description('Show the project filesystem structure')
.action(async () => {
let spinner;
if (ctx.config.output !== 'json') {
spinner = Console.spinner('Fetching filesystem structure...');
}
try {
const response = await ctx.client.get('/api/v1/_config/filesystem');
if (spinner) {
spinner.succeed(chalk.green('✓ Filesystem structure retrieved'));
}
const data = response.data;

if (ctx.config.output !== 'json') {
Console.info(chalk.cyan('\n📁 Project Filesystem'));
Console.info(chalk.gray('═'.repeat(60)));
}
renderJson(data, ctx.config.jsonStyle);
} catch (error) {
if (spinner) {
spinner.fail(chalk.red('✗ Failed to fetch filesystem structure'));
}
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
37 changes: 37 additions & 0 deletions cli/src/commands/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,43 @@ export function registerEndpointCommands(program: Command, ctx: CliContext) {
}
});

endpoints
.command('parameters <path>')
.description('Get parameter definitions for an endpoint')
.option('--output <format>', 'Output format: json or table')
.action(async (path: string, options: { output?: 'json' | 'table' }) => {
const spinner = Console.spinner(`Fetching parameters for ${path}...`);
try {
const endpointUrl = buildEndpointUrl(path, 'parameters');
const response = await ctx.client.get(endpointUrl);
spinner.succeed(chalk.green(`✓ Parameters for ${path} retrieved`));
const data = response.data;
const resolved = applyOutputOverride(ctx.config, options.output);
if (resolved.output === 'json') {
renderJson(data, resolved.jsonStyle);
} else {
Console.info(chalk.cyan(`\n🔧 Parameters: ${path}`));
Console.info(chalk.gray('═'.repeat(60)));
const params = Array.isArray(data?.parameters) ? data.parameters : [];
if (params.length === 0) {
Console.info(chalk.gray('No parameters defined'));
} else {
params.forEach((p: any) => {
const req = p.required ? chalk.red('required') : chalk.gray('optional');
Console.info(chalk.bold.blue(p.name) + chalk.gray(` (in: ${p.in})`) + ' ' + req);
if (p.description) {
Console.info(chalk.gray(` ${p.description}`));
}
});
}
}
} catch (error) {
spinner.fail(chalk.red(`✗ Failed to fetch parameters for ${path}`));
handleError(error, ctx.config);
process.exitCode = 1;
}
});

withPayloadOptions(
endpoints
.command('create')
Expand Down
44 changes: 44 additions & 0 deletions cli/src/commands/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 registerHealthCommand(program: Command, ctx: CliContext) {
program
.command('health')
.description('Get server health (database, endpoints, Arrow IPC, VFS, credential status)')
.action(async () => {
let spinner;
if (ctx.config.output !== 'json') {
spinner = Console.spinner('Fetching server health...');
}
try {
const response = await ctx.client.get('/api/v1/_config/health');
if (spinner) {
spinner.succeed(chalk.green('✓ Server health retrieved'));
}
const data = response.data;

if (ctx.config.output === 'json') {
renderJson(data, ctx.config.jsonStyle);
} else {
Console.info(chalk.cyan('\n❤️ Server Health'));
Console.info(chalk.gray('═'.repeat(60)));
const status = data?.status ?? 'unknown';
const healthy = ['ok', 'healthy', 'up'].includes(String(status).toLowerCase());
Console.info(
chalk.bold.blue('Status: ') + (healthy ? chalk.green(status) : chalk.yellow(status)),
);
renderJson(data, ctx.config.jsonStyle);
}
} catch (error) {
if (spinner) {
spinner.fail(chalk.red('✗ Failed to fetch server health'));
}
handleError(error, ctx.config);
process.exitCode = 1;
}
});
}
2 changes: 2 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { loadConfig } from './lib/config';
import { createApiClient } from './lib/http';
import { handleError } from './lib/errors';
import { registerPingCommand } from './commands/ping';
import { registerHealthCommand } from './commands/health';
import { registerConfigCommands } from './commands/config';
import { registerProjectCommands } from './commands/project';
import { registerEndpointCommands } from './commands/endpoints';
Expand Down Expand Up @@ -55,6 +56,7 @@ export async function createCli(argv = process.argv) {
};

registerPingCommand(program, ctx);
registerHealthCommand(program, ctx);
registerConfigCommands(program, ctx);
registerProjectCommands(program, ctx);
registerEndpointCommands(program, ctx);
Expand Down
2 changes: 2 additions & 0 deletions cli/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@
}
},
"scripts": {
"build:shared": "npm --prefix ../shared run build",
"prebuild": "npm run build:shared",
"build": "webpack --mode production && npm run build:webview",
"dev": "webpack --mode development --watch",
"build:webview": "webpack --config webview/webpack.config.js --mode production",
Expand Down
20 changes: 10 additions & 10 deletions cli/vscode-extension/src/webview/endpointTesterPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1942,27 +1942,27 @@ export class EndpointTesterPanel {
let summaryHTML = '<div style="font-weight: bold; margin-bottom: 8px; color: var(--vscode-activityBarBadge-background);">📝 Write Operation Result</div>';

if (parsedData.rows_affected !== undefined) {
summaryHTML += `<div style="margin: 4px 0;"><strong>Rows Affected:</strong> <span style="color: var(--vscode-textLink-foreground);">${parsedData.rows_affected}</span></div>`;
summaryHTML += \`<div style="margin: 4px 0;"><strong>Rows Affected:</strong> <span style="color: var(--vscode-textLink-foreground);">\${parsedData.rows_affected}</span></div>\`;
}

if (parsedData.last_insert_id !== undefined) {
summaryHTML += `<div style="margin: 4px 0;"><strong>Last Insert ID:</strong> <span style="color: var(--vscode-textLink-foreground);">${parsedData.last_insert_id}</span></div>`;
summaryHTML += \`<div style="margin: 4px 0;"><strong>Last Insert ID:</strong> <span style="color: var(--vscode-textLink-foreground);">\${parsedData.last_insert_id}</span></div>\`;
}

if (parsedData.returned_data && Array.isArray(parsedData.returned_data) && parsedData.returned_data.length > 0) {
summaryHTML += `<div style="margin: 4px 0;"><strong>Returned Data:</strong> ${parsedData.returned_data.length} record(s)</div>`;
summaryHTML += \`<div style="margin: 4px 0;"><strong>Returned Data:</strong> \${parsedData.returned_data.length} record(s)</div>\`;
}

if (parsedData.errors && Array.isArray(parsedData.errors) && parsedData.errors.length > 0) {
summaryHTML += `<div style="margin: 8px 0; padding: 8px; background: var(--vscode-inputValidation-errorBackground); border-radius: 4px;">`;
summaryHTML += \`<div style="margin: 8px 0; padding: 8px; background: var(--vscode-inputValidation-errorBackground); border-radius: 4px;">\`;
summaryHTML += '<div style="font-weight: bold; color: var(--vscode-errorForeground); margin-bottom: 4px;">⚠️ Validation Errors:</div>';
parsedData.errors.forEach(error => {
summaryHTML += `<div style="margin: 2px 0; color: var(--vscode-errorForeground);">• ${error.field || 'Unknown'}: ${error.message || 'Error'}</div>`;
summaryHTML += \`<div style="margin: 2px 0; color: var(--vscode-errorForeground);">• \${error.field || 'Unknown'}: \${error.message || 'Error'}</div>\`;
});
summaryHTML += '</div>';
} else if (parsedData.error) {
summaryHTML += `<div style="margin: 8px 0; padding: 8px; background: var(--vscode-inputValidation-errorBackground); border-radius: 4px;">`;
summaryHTML += `<div style="color: var(--vscode-errorForeground);">⚠️ Error: ${parsedData.error.field || 'Unknown'}: ${parsedData.error.message || 'Error'}</div>`;
summaryHTML += \`<div style="margin: 8px 0; padding: 8px; background: var(--vscode-inputValidation-errorBackground); border-radius: 4px;">\`;
summaryHTML += \`<div style="color: var(--vscode-errorForeground);">⚠️ Error: \${parsedData.error.field || 'Unknown'}: \${parsedData.error.message || 'Error'}</div>\`;
summaryHTML += '</div>';
}

Expand Down
Loading
Loading