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
63 changes: 63 additions & 0 deletions .github/workflows/clients.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Clients (CLI + VSCode)

# Compile/test gate for the TypeScript packages under cli/. This exists so a broken
# build (like the extension being uncompilable for months) is caught on every PR.

on:
push:
branches: [ main ]
paths:
- 'cli/**'
- '.github/workflows/clients.yaml'
pull_request:
branches: [ main ]
paths:
- 'cli/**'
- '.github/workflows/clients.yaml'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
shared:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build @flapi/shared
working-directory: cli/shared
run: |
npm ci
npm run build

cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build & test flapii CLI
working-directory: cli
run: |
npm ci
npm run build
npm test

vscode-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build, typecheck & test the VSCode extension
working-directory: cli/vscode-extension
run: |
npm ci
npm run typecheck
npm run build
npm test
6 changes: 3 additions & 3 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flapii",
"version": "0.1.0",
"version": "26.6.13",
"description": "CLI client for flapi ConfigService",
"license": "MIT",
"type": "module",
Expand Down
7 changes: 2 additions & 5 deletions cli/shared/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions cli/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "@flapi/shared",
"version": "0.1.0",
"version": "26.6.13",
"type": "module",
"main": "dist/index.js",
"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
52 changes: 48 additions & 4 deletions cli/shared/src/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { pathToSlug, slugToPath } from './lib/url';
import type { ValidationResult, ReloadResult } from './lib/types';

/**
* Configuration options for FlapiApiClient
Expand Down Expand Up @@ -188,8 +189,14 @@ export class FlapiApiClient {
/**
* Update authentication token
*/
setToken(token: string) {
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setToken(token: string | undefined) {
if (token) {
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
this.client.defaults.headers.common['X-Config-Token'] = token;
} else {
delete this.client.defaults.headers.common['Authorization'];
delete this.client.defaults.headers.common['X-Config-Token'];
}
if (this.debug) {
console.log('[FlapiAPI] Token updated');
}
Expand Down Expand Up @@ -257,12 +264,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 Expand Up @@ -345,6 +354,41 @@ export class FlapiApiClient {
const response = await this.client.get('/api/v1/_config/endpoints');
return response.data;
}

/**
* Validate raw endpoint YAML for an already-encoded endpoint slug.
* A 400 (invalid config) is returned as a normalized result rather than thrown.
*/
async validateEndpointConfig(slug: string, yamlContent: string): Promise<ValidationResult> {
const response = await this.client.post(
`/api/v1/_config/endpoints/${encodeURIComponent(slug)}/validate`,
yamlContent,
{
headers: { 'Content-Type': 'application/x-yaml' },
validateStatus: (status) => status < 500,
},
);
const result = response.data ?? {};
return {
valid: result.valid ?? false,
errors: result.errors ?? [],
warnings: result.warnings ?? [],
};
}

/**
* Reload an endpoint configuration from disk by already-encoded slug.
*/
async reloadEndpointConfig(slug: string): Promise<ReloadResult> {
const response = await this.client.post(
`/api/v1/_config/endpoints/${encodeURIComponent(slug)}/reload`,
);
const result = response.data ?? {};
return {
success: result.success ?? false,
message: result.message ?? '',
};
}
}

/**
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;
}
});
}
Loading
Loading