diff --git a/cli/shared/package.json b/cli/shared/package.json index 5c26504a..26f8a8e9 100644 --- a/cli/shared/package.json +++ b/cli/shared/package.json @@ -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", diff --git a/cli/shared/src/apiClient.ts b/cli/shared/src/apiClient.ts index 6fffee1b..53d6569c 100644 --- a/cli/shared/src/apiClient.ts +++ b/cli/shared/src/apiClient.ts @@ -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): Promise { 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; diff --git a/cli/src/commands/cache/index.ts b/cli/src/commands/cache/index.ts index 260c7151..34595870 100644 --- a/cli/src/commands/cache/index.ts +++ b/cli/src/commands/cache/index.ts @@ -275,4 +275,51 @@ export function registerCacheCommands(program: Command, ctx: CliContext) { process.exitCode = 1; } }); + + cache + .command('gc ') + .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; + } + }); } diff --git a/cli/src/commands/config/index.ts b/cli/src/commands/config/index.ts index be789926..f04d9680 100644 --- a/cli/src/commands/config/index.ts +++ b/cli/src/commands/config/index.ts @@ -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); } diff --git a/cli/src/commands/config/info.ts b/cli/src/commands/config/info.ts new file mode 100644 index 00000000..c93ab5f4 --- /dev/null +++ b/cli/src/commands/config/info.ts @@ -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; + } + }); +} diff --git a/cli/src/commands/endpoints/index.ts b/cli/src/commands/endpoints/index.ts index e09f7854..d5d51950 100644 --- a/cli/src/commands/endpoints/index.ts +++ b/cli/src/commands/endpoints/index.ts @@ -63,6 +63,43 @@ export function registerEndpointCommands(program: Command, ctx: CliContext) { } }); + endpoints + .command('parameters ') + .description('Get parameter definitions for an endpoint') + .option('--output ', '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') diff --git a/cli/src/commands/health.ts b/cli/src/commands/health.ts new file mode 100644 index 00000000..009707f6 --- /dev/null +++ b/cli/src/commands/health.ts @@ -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; + } + }); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index f7442b97..74b477a3 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -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'; @@ -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); diff --git a/cli/vscode-extension/package.json b/cli/vscode-extension/package.json index baf8d172..eeb50979 100644 --- a/cli/vscode-extension/package.json +++ b/cli/vscode-extension/package.json @@ -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", diff --git a/cli/vscode-extension/src/webview/endpointTesterPanel.ts b/cli/vscode-extension/src/webview/endpointTesterPanel.ts index c8388412..148abfaf 100644 --- a/cli/vscode-extension/src/webview/endpointTesterPanel.ts +++ b/cli/vscode-extension/src/webview/endpointTesterPanel.ts @@ -1942,27 +1942,27 @@ export class EndpointTesterPanel { let summaryHTML = '
๐Ÿ“ Write Operation Result
'; if (parsedData.rows_affected !== undefined) { - summaryHTML += `
Rows Affected: ${parsedData.rows_affected}
`; + summaryHTML += \`
Rows Affected: \${parsedData.rows_affected}
\`; } - + if (parsedData.last_insert_id !== undefined) { - summaryHTML += `
Last Insert ID: ${parsedData.last_insert_id}
`; + summaryHTML += \`
Last Insert ID: \${parsedData.last_insert_id}
\`; } - + if (parsedData.returned_data && Array.isArray(parsedData.returned_data) && parsedData.returned_data.length > 0) { - summaryHTML += `
Returned Data: ${parsedData.returned_data.length} record(s)
`; + summaryHTML += \`
Returned Data: \${parsedData.returned_data.length} record(s)
\`; } - + if (parsedData.errors && Array.isArray(parsedData.errors) && parsedData.errors.length > 0) { - summaryHTML += `
`; + summaryHTML += \`
\`; summaryHTML += '
โš ๏ธ Validation Errors:
'; parsedData.errors.forEach(error => { - summaryHTML += `
โ€ข ${error.field || 'Unknown'}: ${error.message || 'Error'}
`; + summaryHTML += \`
โ€ข \${error.field || 'Unknown'}: \${error.message || 'Error'}
\`; }); summaryHTML += '
'; } else if (parsedData.error) { - summaryHTML += `
`; - summaryHTML += `
โš ๏ธ Error: ${parsedData.error.field || 'Unknown'}: ${parsedData.error.message || 'Error'}
`; + summaryHTML += \`
\`; + summaryHTML += \`
โš ๏ธ Error: \${parsedData.error.field || 'Unknown'}: \${parsedData.error.message || 'Error'}
\`; summaryHTML += '
'; } diff --git a/docs/CLIENT_PARITY_AUDIT.md b/docs/CLIENT_PARITY_AUDIT.md new file mode 100644 index 00000000..63b83f3a --- /dev/null +++ b/docs/CLIENT_PARITY_AUDIT.md @@ -0,0 +1,192 @@ +# Client Parity Audit โ€” flapii CLI & VSCode Extension vs. flapi Server + +**Date:** 2026-06-14 +**Server reference:** tag `v26.06.13` (binary tested: `build/release/flapi`, built 2026-06-11, self-reported version `1.0`) +**flapii CLI:** `cli/` โ€” version `0.1.0` +**VSCode extension:** `cli/vscode-extension/` โ€” version `0.1.0` +**Shared package:** `cli/shared/` (`@flapi/shared`) โ€” version `0.1.0` + +Scope: **audit + verify only** โ€” no feature changes were made. The clients were +built and exercised against a live server; results below are backed by either a +live request or a source citation. + +--- + +## TL;DR + +| Client | Builds? | Tests | Live behaviour | On par with server? | +|--------|---------|-------|----------------|---------------------| +| **flapii CLI** | โœ… yes (tsup) | โœ… 41/41 unit pass; โš ๏ธ integration blocked by server-binary crash (not a CLI bug) | โœ… all exercised commands work against live server | **Mostly** โ€” works, but missing ~6 commands the server now supports | +| **VSCode extension** | โŒ **NO** โ€” webpack build fails | โŒ no test suite exists | โ›” cannot be packaged/run | **No** โ€” broken in-repo since 2025-11-02 | + +**Headline:** The CLI is healthy and largely keeps up with the server, with a +handful of missing commands. The **VSCode extension does not compile** and has not +compiled since its last commit (`fcebf3a`, "First draft of write support", +2025-11-02) โ€” it is effectively unshippable today. + +--- + +## 1. Build / Run status (evidence) + +### Server +- `build/release/flapi` boots with `--config-service` and serves the full ConfigService + API (verified by live HTTP probes below). +- **Caveat 1 (environment):** the bundled `examples/data/cache.ducklake` is a stale + DuckLake catalog (version 0.3 vs extension 1.0) and aborts startup + (`DuckLake catalog version mismatch`). Worked around by moving it aside (restorable + backup in `/tmp/flapi-ducklake-bak/`). +- **Caveat 2 (instability):** under the CLI integration harness the server binary + crashes on startup with **varying signals** (SIGSEGV / SIGABRT / SIGBUS). The same + binary runs fine when launched manually with the same config, so this looks like + binary/environment staleness โ€” the working tree has a **modified `duckdb` submodule** + (`git status: M duckdb`) that the 2026-06-11 prebuilt binary predates. Not a client defect. + +### flapii CLI +- `npm ci` โœ…, `npm run build` (tsup) โœ… โ€” `ESM build success`. +- `npm test` (Vitest, nock-mocked): **41/41 pass** (8 files). *(One spurious failure only + appears if `FLAPI_BASE_URL` is exported into the test env โ€” a test-isolation quirk in + `test/unit/config.spec.ts`, not a product bug.)* +- `npm run test:integration`: **blocked** โ€” `test/integration/setup.ts` spawns the real + `build/release/flapi`, which crashes (see Caveat 2). 19 tests skipped, 0 ran. The harness + itself is correct; it is gated on a working server binary. +- **Live manual smoke test** against a manually-launched server (`:8099`, `--config-service`): + `ping`, `endpoints list`, `endpoints get`, `templates get`, `templates expand`, + `cache get`, `schema connections`, `config log-level get`, `mcp tools list` โ€” **all exit 0 + and return correct data.** + +### VSCode extension +- `npm ci` โœ…. +- `npm run build` (webpack ext + webview): โŒ **FAILS, exit 1.** +- `npx tsc --noEmit`: โŒ fails (same errors). +- Two error classes, both in **committed** code (no local modifications), both from commit + `fcebf3a` (2025-11-02): + 1. `src/providers/endpointsProvider.ts:60,61,79,80` โ€” + `TS2339: Property 'operation' does not exist on type 'EndpointConfig'` (type drift: the + provider reads `endpoint.operation`, which the `EndpointConfig` type no longer declares). + 2. `src/webview/endpointTesterPanel.ts:1895โ€“2030` โ€” cascade of `TS1005 / TS1110 / TS1127` + ("`;` expected", "Type expected", "Invalid character"). A malformed/un-terminated + template literal in the "write operation result" block desyncs the parser. This is the + half-finished "write support" draft. + +--- + +## 2. Parity matrix (server capability โ†’ client coverage) + +Legend: โœ… exposed ยท โš ๏ธ partial / indirect ยท โŒ not exposed +The extension column reflects **source intent**; remember the extension **does not build**, so +every โœ… there is currently non-functional. + +| Server capability (ConfigService / MCP) | Server route | flapii CLI | VSCode ext | +|---|---|---|---| +| Project config (get) | `GET /_config/project` | โœ… `project get` / `ping` | โœ… openProject | +| Project config (update) | `PUT /_config/project` | โŒ | โŒ โ€” *(server returns 501; not implemented)* | +| Environment variables | `GET /_config/environment-variables` | โŒ | โš ๏ธ client method exists; UI ("Variables") removed | +| Endpoints โ€” list | `GET /_config/endpoints` | โœ… `endpoints list` | โœ… explorer | +| Endpoints โ€” get | `GET /_config/endpoints/{slug}` | โœ… `endpoints get` | โœ… | +| Endpoints โ€” create/update/delete | `POST/PUT/DELETE โ€ฆ/{slug}` | โœ… `endpoints create/update/delete` | โš ๏ธ create only (`newEndpoint`) | +| Endpoints โ€” validate | `POST โ€ฆ/{slug}/validate` | โœ… `endpoints validate` | โœ… validateYaml | +| Endpoints โ€” reload | `POST โ€ฆ/{slug}/reload` | โœ… `endpoints reload` | โœ… reloadEndpoint | +| Endpoints โ€” parameters | `GET โ€ฆ/{slug}/parameters` | โŒ | โœ… openParameters | +| Endpoints โ€” by-template | `POST โ€ฆ/by-template` | โŒ | โœ… (used by SQL tester) | +| Template โ€” get/update | `GET/PUT โ€ฆ/{slug}/template` | โœ… `templates get/update` | โœ… | +| Template โ€” expand | `POST โ€ฆ/{slug}/template/expand` | โœ… `templates expand` | โœ… expandTemplate | +| Template โ€” test (exec) | `POST โ€ฆ/{slug}/template/test` | โœ… `templates test` | โœ… testTemplate | +| Cache โ€” get/update | `GET/PUT โ€ฆ/{slug}/cache` | โœ… `cache get/update` | โœ… openCache | +| Cache โ€” template get/update | `GET/PUT โ€ฆ/{slug}/cache/template` | โœ… `cache template / update-template` | โš ๏ธ | +| Cache โ€” refresh | `POST โ€ฆ/{slug}/cache/refresh` | โœ… `cache refresh` | โŒ | +| **Cache โ€” GC** | `POST โ€ฆ/{slug}/cache/gc` | โŒ | โŒ | +| **Cache โ€” audit (per-endpoint)** | `GET โ€ฆ/{slug}/cache/audit` | โŒ | โŒ | +| **Cache โ€” audit (global)** | `GET /_config/cache/audit` | โŒ | โŒ | +| Schema โ€” get | `GET /_config/schema` | โœ… `schema get/connections/tables` | โœ… schema browser | +| Schema โ€” refresh | `POST /_config/schema/refresh` | โœ… `schema refresh` | โœ… schema.refresh | +| Log level โ€” get/set | `GET/PUT /_config/log-level` | โœ… `config log-level get/set/list` | โŒ | +| **Filesystem tree** | `GET /_config/filesystem` | โŒ | โš ๏ธ client method exists | +| **Health / metrics** | `GET /_config/health` | โŒ (`ping` hits `/project`) | โŒ | +| OpenAPI doc | `GET /doc.yaml` | โŒ | โŒ | +| MCP tools/resources/prompts (user-defined) | `GET /_config/endpoints` (MCP slugs) | โœ… `mcp tools/resources/prompts list/get` | โœ… openMcpItem | +| MCP JSON-RPC protocol (`/mcp/jsonrpc`, `/mcp/health`) | โ€” | โŒ (no MCP client/exec) | โŒ | +| 20 `flapi_*` MCP config tools (`--config-service`) | via `/mcp/jsonrpc` | โž– N/A (consumed by MCP clients, not the CLI) | โž– N/A | +| Self-packaging (`pack`/`info`/`unpack`) | server-binary subcommands | โž– N/A (server binary, not flapii) | โž– N/A | + +All "โŒ on server route" cells were confirmed live: the route returns **200** while the CLI +reports `error: unknown command` (verified for `cache gc`, `cache audit`, `environment-variables`, +`filesystem`, `health`). + +--- + +## 3. Findings, prioritized + +### P0 โ€” Broken / unshippable +1. **VSCode extension does not compile.** Webpack + `tsc` both fail. Two root causes + (`endpointsProvider.ts` type drift on `operation`; `endpointTesterPanel.ts` malformed + template literal in the write-support draft). Broken in-repo since 2025-11-02. The extension + cannot be packaged or run until fixed. *(Files: `cli/vscode-extension/src/providers/endpointsProvider.ts:60-80`, + `cli/vscode-extension/src/webview/endpointTesterPanel.ts:1895-2030`.)* + +### P1 โ€” Functional parity gaps (server has it; clients don't) +2. **CLI missing commands** for live server capabilities: `cache gc`, `cache audit` + (per-endpoint + global), `endpoints parameters`, `environment-variables`, `filesystem`, + and a real `health` command (today `ping` queries `/project`, not `/health`, so it never + surfaces DB / Arrow-IPC / VFS / credential status). +3. **Dead/broken client method:** `FlapiApiClient.testEndpoint()` + (`cli/shared/src/apiClient.ts:262`) POSTs to `/_config/endpoints/{slug}/test`, which **does + not exist** on the server (only `/template/test` does, `config_service.cpp:393`). Any caller + would get a 404. The CLI's `templates test` correctly targets `template/test`, so the method + is currently unused โ€” but it is a latent landmine. + +### P2 โ€” Architectural drift (violates CLAUDE.md "share a common API client") +4. **Two divergent `FlapiApiClient` implementations.** `cli/shared/src/apiClient.ts` (axios, + full surface, used by the CLI) vs. `cli/vscode-extension/src/shared/apiClient.ts` (separate + impl, validate/reload/template subset). The mandated single shared client does not exist in + practice; the extension does not consume `@flapi/shared`'s client. +5. **Dead code in the extension:** the "Variables provider" was removed but its refresh call / + wiring remains (`extension.ts` ~898-908); unused `include_variables` fetch. +6. **Stubbed TODOs in the extension:** single-YAML backend parse + (`codelens/endpointTestProvider.ts:186`), multi-endpoint selector + (`webview/sqlTemplateTesterPanel.ts:118`). + +### P3 โ€” Hygiene / staleness +7. **No test suite for the extension** (no framework, no specs) โ€” the broken build would have + been caught by even a smoke compile in CI. +8. **Version skew:** server `v26.06.13`; all three JS packages pinned at `0.1.0`. Extension + untouched ~7 months; CLI's last feature work 2026-01-16. +9. **Examples ship a stale DuckLake catalog** that aborts a fresh server boot (env, but + user-facing for anyone running `examples/`). + +--- + +## 4. Verdict & minimum work to reach parity + +**flapii CLI โ€” "on par": mostly yes.** It builds, its unit tests pass, and every exercised +command works against a live server. To call it fully on par, add the six missing commands in +P1 #2 (the shared client already has methods for `parameters`, `environment-variables`, +`filesystem`), fix the `testEndpoint()` route in #3, and wire a real `health` command. + +**VSCode extension โ€” "on par": no.** It is broken at the build level and cannot run. Minimum to +restore: fix the two compile errors (P0 #1), then re-assess parity โ€” on paper its feature set +is actually *wider* than the CLI's (parameters, by-template, interactive testers), but none of +it ships until it compiles. Strongly recommend adding a CI compile/typecheck gate (P3 #7) and +collapsing onto the single shared API client (P2 #4). + +--- + +## 5. Resolution + +The P0/P1 findings were filed and fixed in a single PR: + +- **#75** (P0) โ€” VSCode extension now builds: escaped the nested template literals in + `endpointTesterPanel.ts`, and made `@flapi/shared` build automatically (`prepare` on shared, + `prebuild` on the extension) so its types can't go stale. `npm run build` + `tsc --noEmit` both pass. +- **#76** (P1) โ€” added the missing CLI commands: `health`, `config env`, `config filesystem`, + `endpoints parameters `, `cache audit [path]`, `cache gc `. All verified against a + live server. +- **#77** (P1) โ€” `FlapiApiClient.testEndpoint()` now targets the real `โ€ฆ/template/test` route. + +P2/P3 items (single shared API client, extension test suite + CI compile gate, version alignment, +stale example DuckLake catalog) remain open follow-ups, intentionally out of this PR's scope. + +## Reproduction notes +- Server (manual): `./build/release/flapi -c examples/flapi.yaml --port 8099 --config-service --config-service-token testtoken123` + (after moving `examples/data/cache.ducklake*` aside). +- CLI: `cd cli && npm ci && npm run build && npm test`; live: `FLAPI_BASE_URL=โ€ฆ FLAPI_CONFIG_SERVICE_TOKEN=โ€ฆ node dist/index.js `. +- Extension: `cd cli/vscode-extension && npm ci && npm run build` (fails) / `npx tsc --noEmit` (fails).