diff --git a/apps/docs/content/docs/fmodata/cli.mdx b/apps/docs/content/docs/fmodata/cli.mdx new file mode 100644 index 0000000..8908f55 --- /dev/null +++ b/apps/docs/content/docs/fmodata/cli.mdx @@ -0,0 +1,360 @@ +--- +title: CLI +description: Run fmodata operations from the command line — queries, scripts, webhooks, metadata, and schema changes +--- + +import { Callout } from "fumadocs-ui/components/callout"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; + +The `@proofkit/fmodata` package ships a built-in CLI binary called **`fmodata`**. It exposes every library operation — querying records, running scripts, managing webhooks, inspecting metadata, and modifying schema — as a non-interactive command suitable for scripting, CI pipelines, and quick one-off database operations. + +## Installation + +The binary is included automatically when you install the package: + +```bash +pnpm add @proofkit/fmodata +# or +npm install @proofkit/fmodata +``` + +If you want it available globally: + +```bash +pnpm add -g @proofkit/fmodata +``` + +## Connection Configuration + +All commands share the same global connection options. Each flag has an environment variable fallback so you can set credentials once and run many commands. + +| Flag | Env var | Description | +|---|---|---| +| `--server ` | `FM_SERVER` | FileMaker Server URL (e.g. `https://fm.example.com`) | +| `--database ` | `FM_DATABASE` | Database filename (e.g. `MyApp.fmp12`) | +| `--username ` | `FM_USERNAME` | FileMaker account username | +| `--password ` | `FM_PASSWORD` | FileMaker account password | +| `--api-key ` | `OTTO_API_KEY` | OttoFMS API key (preferred over username/password) | + + +When both `--api-key` and `--username` are present, the API key is used. + + +**Example — using environment variables:** + +```bash +export FM_SERVER=https://fm.example.com +export FM_DATABASE=MyApp.fmp12 +export OTTO_API_KEY=otto_... + +fmodata metadata tables +``` + +**Example — passing flags directly:** + +```bash +fmodata --server https://fm.example.com \ + --database MyApp.fmp12 \ + --api-key otto_... \ + metadata tables +``` + +## Output Formats + +By default all commands print **JSON** to stdout. Add `--table` to render results as a human-readable ASCII table instead. + +```bash +# JSON (default) +fmodata metadata tables + +# ASCII table +fmodata metadata tables --table +``` + +Errors are written to **stderr** and the process exits with code `1`. + +--- + +## Commands + +### `query` + +CRUD operations against any table. Use `--table-name` to avoid ambiguity with the `--table` output flag. + +#### `query list` + +Fetch records from a table. + +```bash +fmodata query list --table-name contacts +fmodata query list --table-name contacts --top 10 --skip 20 +fmodata query list --table-name contacts --select "name,email" +fmodata query list --table-name contacts --where "name eq 'Alice'" +fmodata query list --table-name contacts --order-by "name:asc" +fmodata query list --table-name contacts --order-by "createdAt:desc,name:asc" +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Table to query | +| `--top ` | Maximum records to return | +| `--skip ` | Records to skip (for pagination) | +| `--select ` | Comma-separated field names | +| `--where ` | OData `$filter` expression | +| `--order-by ` | `field:asc` or `field:desc`, comma-separated for multi-sort | + +#### `query insert` + +Insert a single record. + +```bash +fmodata query insert --table-name contacts --data '{"name":"Alice","email":"alice@example.com"}' +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Target table | +| `--data ` | **Required.** Record fields as a JSON object | + +#### `query update` + +Update records matching a filter (or all records if `--where` is omitted). + +```bash +fmodata query update \ + --table-name contacts \ + --data '{"status":"inactive"}' \ + --where "lastLogin lt 2024-01-01" +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Target table | +| `--data ` | **Required.** Fields to update as a JSON object | +| `--where ` | OData `$filter` expression (omit to update all rows) | + +#### `query delete` + +Delete records matching a filter. + +```bash +fmodata query delete --table-name contacts --where "status eq 'archived'" +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Target table | +| `--where ` | OData `$filter` expression | + + +Omitting `--where` from `query delete` will delete **all records** in the table. + + +--- + +### `script` + +#### `script run` + +Execute a FileMaker script and print the result code and return value. + +```bash +# No parameter +fmodata script run MyScriptName + +# String parameter +fmodata script run SendEmail --param '"hello@example.com"' + +# JSON object parameter +fmodata script run ProcessOrder --param '{"orderId":"123","action":"approve"}' +``` + +| Option | Description | +|---|---| +| `--param ` | Script parameter — parsed as JSON, falls back to plain string | + +The output is a JSON object: + +```json +{ + "resultCode": 0, + "result": "optional-return-value" +} +``` + + +OData does not support script names with special characters (`@`, `&`, `/`) or names beginning with a number. + + +--- + +### `webhook` + +#### `webhook list` + +List all webhooks registered on the database. + +```bash +fmodata webhook list +fmodata webhook list --table +``` + +#### `webhook get` + +Get details for a specific webhook by its numeric ID. + +```bash +fmodata webhook get 42 +``` + +#### `webhook add` + +Register a new webhook on a table. + +```bash +fmodata webhook add \ + --table-name contacts \ + --url https://example.com/hooks/contacts + +# With field selection and custom headers +fmodata webhook add \ + --table-name contacts \ + --url https://example.com/hooks/contacts \ + --select "name,email,modifiedAt" \ + --header "Authorization=Bearer token123" \ + --header "X-App-ID=my-app" +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Table to monitor | +| `--url ` | **Required.** Webhook endpoint URL | +| `--select ` | Comma-separated field names to include in the payload | +| `--header ` | Custom request header in `key=value` format (repeatable) | + +#### `webhook remove` + +Delete a webhook by ID. + +```bash +fmodata webhook remove 42 +``` + +--- + +### `metadata` + +#### `metadata get` + +Retrieve OData metadata for the database. + +```bash +# JSON (default) +fmodata metadata get + +# XML +fmodata metadata get --format xml +``` + +| Option | Description | +|---|---| +| `--format ` | `json` (default) or `xml` | + +#### `metadata tables` + +List all table names in the database. This is the quickest way to inspect what's available. + +```bash +fmodata metadata tables +fmodata metadata tables --table +``` + +--- + +### `schema` + +Schema modification commands are **safe by default**: without `--confirm` they perform a **dry run** and print what _would_ happen without making any changes. + +#### `schema list-tables` + +List all tables (alias for `metadata tables`). + +```bash +fmodata schema list-tables +``` + +#### `schema create-table` + +Create a new table. The `--fields` option accepts the same JSON field definition used by the TypeScript API. + +```bash +# Dry run (no changes) +fmodata schema create-table \ + --name NewTable \ + --fields '[{"name":"id","type":"string","primary":true},{"name":"label","type":"string"}]' + +# Execute for real +fmodata schema create-table \ + --name NewTable \ + --fields '[{"name":"id","type":"string","primary":true},{"name":"label","type":"string"}]' \ + --confirm +``` + +| Option | Description | +|---|---| +| `--name ` | **Required.** New table name | +| `--fields ` | **Required.** Array of field definitions (see [Schema Management](/docs/fmodata/schema-management)) | +| `--confirm` | Execute the operation (without this flag it's a dry run) | + +#### `schema add-fields` + +Add fields to an existing table. + +```bash +# Dry run +fmodata schema add-fields \ + --table-name contacts \ + --fields '[{"name":"phone","type":"string","nullable":true}]' + +# Execute +fmodata schema add-fields \ + --table-name contacts \ + --fields '[{"name":"phone","type":"string","nullable":true}]' \ + --confirm +``` + +| Option | Description | +|---|---| +| `--table-name ` | **Required.** Existing table name | +| `--fields ` | **Required.** Array of field definitions | +| `--confirm` | Execute the operation (without this flag it's a dry run) | + + +Creating tables and adding fields require a FileMaker account with DDL (Data Definition Language) privileges. Operations will throw an error if the account lacks sufficient permissions. + + +--- + +## Using in CI / Scripts + +Because all connection options accept environment variables, the CLI integrates cleanly into CI pipelines: + +```bash +# GitHub Actions example +- name: Run post-deploy script + env: + FM_SERVER: ${{ secrets.FM_SERVER }} + FM_DATABASE: ${{ secrets.FM_DATABASE }} + OTTO_API_KEY: ${{ secrets.OTTO_API_KEY }} + run: | + npx fmodata script run PostDeploy --param '"${{ github.sha }}"' +``` + +```bash +# Quick schema check in a shell script +#!/usr/bin/env bash +set -euo pipefail + +TABLES=$(fmodata metadata tables) +echo "Tables in $FM_DATABASE: $TABLES" +``` diff --git a/apps/docs/content/docs/fmodata/meta.json b/apps/docs/content/docs/fmodata/meta.json index eb29546..4b79ab6 100644 --- a/apps/docs/content/docs/fmodata/meta.json +++ b/apps/docs/content/docs/fmodata/meta.json @@ -20,6 +20,8 @@ "entity-ids", "extra-properties", "custom-fetch-handlers", + "---CLI---", + "cli", "---Reference---", "errors", "methods" diff --git a/packages/fmodata/package.json b/packages/fmodata/package.json index 6429619..a74d2ec 100644 --- a/packages/fmodata/package.json +++ b/packages/fmodata/package.json @@ -9,6 +9,9 @@ "type": "module", "main": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", + "bin": { + "fmodata": "dist/cli/index.js" + }, "exports": { ".": { "import": { @@ -19,7 +22,7 @@ "./package.json": "./package.json" }, "scripts": { - "build": "tsc && vite build && publint --strict", + "build": "tsc && vite build && tsdown && publint --strict", "build:watch": "tsc && vite build --watch", "lint": "biome check . --write", "lint:summary": "biome check . --reporter=summary", @@ -31,6 +34,7 @@ "test:build": "pnpm build && TEST_BUILD=true vitest run --typecheck", "test:watch:build": "TEST_BUILD=true vitest --typecheck", "test:e2e": "doppler run -- vitest run tests/e2e", + "test:cli:e2e": "doppler run -- vitest run tests/cli/e2e", "capture": "doppler run -- tsx scripts/capture-responses.ts", "knip": "knip", "pub:alpha": "bun run scripts/publish-alpha.ts", @@ -38,6 +42,7 @@ }, "dependencies": { "@fetchkit/ffetch": "^4.2.0", + "commander": "^14.0.2", "dotenv": "^16.6.1", "es-toolkit": "^1.43.0", "neverthrow": "^8.2.0", @@ -55,8 +60,10 @@ "@standard-schema/spec": "^1.1.0", "@tanstack/vite-config": "^0.2.1", "@types/node": "^22.19.5", + "cli-table3": "^0.6.5", "fast-xml-parser": "^5.3.3", "publint": "^0.3.16", + "tsdown": "^0.14.2", "tsx": "^4.21.0", "typescript": "^5.9.3", "vite": "^6.4.1", diff --git a/packages/fmodata/src/cli/commands/metadata.ts b/packages/fmodata/src/cli/commands/metadata.ts new file mode 100644 index 0000000..ba8cd45 --- /dev/null +++ b/packages/fmodata/src/cli/commands/metadata.ts @@ -0,0 +1,45 @@ +import { Command } from "commander"; +import type { ConnectionOptions } from "../utils/connection"; +import { buildConnection } from "../utils/connection"; +import { handleCliError } from "../utils/errors"; +import { printResult } from "../utils/output"; + +export function makeMetadataCommand(): Command { + const metadata = new Command("metadata").description("FileMaker OData metadata operations"); + + metadata + .command("get") + .description("Get OData metadata for the database") + .option("--format ", "Output format: json or xml", "json") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + let result: unknown; + if (opts.format === "xml") { + result = await db.getMetadata({ format: "xml" }); + } else { + result = await db.getMetadata({ format: "json" }); + } + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + metadata + .command("tables") + .description("List all table names in the database") + .action(async (_opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const tables = await db.listTableNames(); + printResult(tables, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + return metadata; +} diff --git a/packages/fmodata/src/cli/commands/query.ts b/packages/fmodata/src/cli/commands/query.ts new file mode 100644 index 0000000..fd980c2 --- /dev/null +++ b/packages/fmodata/src/cli/commands/query.ts @@ -0,0 +1,139 @@ +import { Command } from "commander"; +import type { ConnectionOptions } from "../utils/connection"; +import { buildConnection } from "../utils/connection"; +import { handleCliError } from "../utils/errors"; +import { printResult } from "../utils/output"; + +function buildQueryString(params: { + top?: number; + skip?: number; + select?: string; + where?: string; + orderBy?: string; +}): string { + const parts: string[] = []; + if (params.top !== undefined) parts.push(`$top=${params.top}`); + if (params.skip !== undefined) parts.push(`$skip=${params.skip}`); + if (params.select) parts.push(`$select=${params.select}`); + if (params.where) parts.push(`$filter=${params.where}`); + if (params.orderBy) { + // Accept "field:asc" or "field:desc" or plain "field" + const orderStr = params.orderBy + .split(",") + .map((part) => { + const [field, dir] = part.trim().split(":"); + return dir ? `${field} ${dir}` : field; + }) + .join(","); + parts.push(`$orderby=${orderStr}`); + } + return parts.length > 0 ? `?${parts.join("&")}` : ""; +} + +export function makeQueryCommand(): Command { + const query = new Command("query").description("FileMaker OData query operations"); + + query + .command("list") + .description("List records from a table") + .requiredOption("--table-name ", "Table name") + .option("--top ", "Max records to return", Number) + .option("--skip ", "Records to skip", Number) + .option("--select ", "Comma-separated field names") + .option("--where ", "OData filter expression") + .option("--order-by ", "Order by field (format: field:asc|desc, or comma-separated)") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const qs = buildQueryString({ + top: opts.top as number | undefined, + skip: opts.skip as number | undefined, + select: opts.select as string | undefined, + where: opts.where as string | undefined, + orderBy: opts.orderBy as string | undefined, + }); + const result = await db._makeRequest<{ value: unknown[] }>(`/${opts.tableName}${qs}`); + if (result.error) throw result.error; + printResult(result.data.value ?? result.data, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + query + .command("insert") + .description("Insert a record into a table") + .requiredOption("--table-name ", "Table name") + .requiredOption("--data ", "Record data as JSON object") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + let data: Record; + try { + data = JSON.parse(opts.data) as Record; + } catch { + throw new Error("--data must be a valid JSON object"); + } + const result = await db._makeRequest(`/${opts.tableName}`, { + method: "POST", + body: JSON.stringify(data), + }); + if (result.error) throw result.error; + printResult(result.data, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + query + .command("update") + .description("Update records in a table") + .requiredOption("--table-name ", "Table name") + .requiredOption("--data ", "Update data as JSON object") + .option("--where ", "OData filter expression") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + let data: Record; + try { + data = JSON.parse(opts.data) as Record; + } catch { + throw new Error("--data must be a valid JSON object"); + } + const qs = opts.where ? `?$filter=${opts.where}` : ""; + const result = await db._makeRequest(`/${opts.tableName}${qs}`, { + method: "PATCH", + body: JSON.stringify(data), + }); + if (result.error) throw result.error; + printResult(result.data, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + query + .command("delete") + .description("Delete records from a table") + .requiredOption("--table-name ", "Table name") + .option("--where ", "OData filter expression") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const qs = opts.where ? `?$filter=${opts.where}` : ""; + const result = await db._makeRequest(`/${opts.tableName}${qs}`, { + method: "DELETE", + }); + if (result.error) throw result.error; + printResult(result.data, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + return query; +} diff --git a/packages/fmodata/src/cli/commands/schema.ts b/packages/fmodata/src/cli/commands/schema.ts new file mode 100644 index 0000000..694e4fc --- /dev/null +++ b/packages/fmodata/src/cli/commands/schema.ts @@ -0,0 +1,86 @@ +import { Command } from "commander"; +import type { Field } from "../../client/schema-manager"; +import type { ConnectionOptions } from "../utils/connection"; +import { buildConnection } from "../utils/connection"; +import { handleCliError } from "../utils/errors"; +import { printResult } from "../utils/output"; + +export function makeSchemaCommand(): Command { + const schema = new Command("schema").description("FileMaker schema modification operations"); + + schema + .command("list-tables") + .description("List all tables in the database") + .action(async (_opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const tables = await db.listTableNames(); + printResult(tables, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + schema + .command("create-table") + .description("Create a new table (requires --confirm to execute; dry-run by default)") + .requiredOption("--name ", "Table name") + .requiredOption("--fields ", "Fields definition as JSON array") + .option("--confirm", "Execute the operation (without this flag, shows what would be created)") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + let fields: Field[]; + try { + fields = JSON.parse(opts.fields) as Field[]; + } catch { + throw new Error("--fields must be a valid JSON array"); + } + + if (!opts.confirm) { + console.log("[dry-run] Would create table:"); + printResult({ tableName: opts.name, fields }, { table: globalOpts.table ?? false }); + return; + } + + const { db } = buildConnection(globalOpts); + const result = await db.schema.createTable(opts.name as string, fields); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + schema + .command("add-fields") + .description("Add fields to an existing table (requires --confirm to execute; dry-run by default)") + .requiredOption("--table-name ", "Table name") + .requiredOption("--fields ", "Fields to add as JSON array") + .option("--confirm", "Execute the operation (without this flag, shows what would be added)") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + let fields: Field[]; + try { + fields = JSON.parse(opts.fields) as Field[]; + } catch { + throw new Error("--fields must be a valid JSON array"); + } + + if (!opts.confirm) { + console.log("[dry-run] Would add fields to table:"); + printResult({ tableName: opts.tableName, fields }, { table: globalOpts.table ?? false }); + return; + } + + const { db } = buildConnection(globalOpts); + const result = await db.schema.addFields(opts.tableName as string, fields); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + return schema; +} diff --git a/packages/fmodata/src/cli/commands/script.ts b/packages/fmodata/src/cli/commands/script.ts new file mode 100644 index 0000000..f50f9d8 --- /dev/null +++ b/packages/fmodata/src/cli/commands/script.ts @@ -0,0 +1,34 @@ +import { Command } from "commander"; +import type { ConnectionOptions } from "../utils/connection"; +import { buildConnection } from "../utils/connection"; +import { handleCliError } from "../utils/errors"; +import { printResult } from "../utils/output"; + +export function makeScriptCommand(): Command { + const script = new Command("script").description("FileMaker script operations"); + + script + .command("run ") + .description("Run a FileMaker script") + .option("--param ", "Script parameter as JSON string or plain value") + .action(async (scriptName: string, opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + let scriptParam: string | number | Record | undefined; + if (opts.param !== undefined) { + try { + scriptParam = JSON.parse(opts.param) as Record; + } catch { + scriptParam = opts.param as string; + } + } + const result = await db.runScript(scriptName, { scriptParam }); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + return script; +} diff --git a/packages/fmodata/src/cli/commands/webhook.ts b/packages/fmodata/src/cli/commands/webhook.ts new file mode 100644 index 0000000..fe6a4aa --- /dev/null +++ b/packages/fmodata/src/cli/commands/webhook.ts @@ -0,0 +1,103 @@ +import { Command } from "commander"; +import type { ConnectionOptions } from "../utils/connection"; +import { buildConnection } from "../utils/connection"; +import { handleCliError } from "../utils/errors"; +import { printResult } from "../utils/output"; + +export function makeWebhookCommand(): Command { + const webhook = new Command("webhook").description("FileMaker webhook operations"); + + webhook + .command("list") + .description("List all webhooks") + .action(async (_opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const result = await db.webhook.list(); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + webhook + .command("get ") + .description("Get a webhook by ID") + .action(async (id: string, _opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + const result = await db.webhook.get(Number(id)); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + webhook + .command("add") + .description("Add a new webhook") + .requiredOption("--table-name ", "Table to monitor") + .requiredOption("--url ", "Webhook URL to call") + .option("--select ", "Comma-separated field names to include") + .option("--header ", "Header in key=value format (repeatable)", (val, acc: string[]) => { + acc.push(val); + return acc; + }, []) + .action(async (opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + + // Parse headers + const headers: Record = {}; + for (const h of opts.header as string[]) { + const eqIdx = h.indexOf("="); + if (eqIdx === -1) { + throw new Error(`Invalid header format (expected key=value): ${h}`); + } + headers[h.slice(0, eqIdx)] = h.slice(eqIdx + 1); + } + + // Build a minimal FMTable-like proxy for the tableName + // webhook.add() only reads the name via Symbol, so this is safe at runtime + const tableProxy = { + [Symbol.for("fmodata:FMTableName")]: opts.tableName, + } as unknown as import("../../orm/table").FMTable, string>; + + const webhookPayload: import("../../client/webhook-builder").Webhook = { + webhook: opts.url as string, + tableName: tableProxy, + }; + + if (Object.keys(headers).length > 0) { + webhookPayload.headers = headers; + } + if (opts.select) { + webhookPayload.select = opts.select as string; + } + + const result = await db.webhook.add(webhookPayload); + printResult(result, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + webhook + .command("remove ") + .description("Remove a webhook by ID") + .action(async (id: string, _opts, cmd) => { + const globalOpts = cmd.parent?.parent?.opts() as ConnectionOptions & { table: boolean }; + try { + const { db } = buildConnection(globalOpts); + await db.webhook.remove(Number(id)); + printResult({ removed: true, id: Number(id) }, { table: globalOpts.table ?? false }); + } catch (err) { + handleCliError(err); + } + }); + + return webhook; +} diff --git a/packages/fmodata/src/cli/index.ts b/packages/fmodata/src/cli/index.ts new file mode 100644 index 0000000..c25e3fc --- /dev/null +++ b/packages/fmodata/src/cli/index.ts @@ -0,0 +1,42 @@ +import { Command } from "commander"; +import { makeMetadataCommand } from "./commands/metadata"; +import { makeQueryCommand } from "./commands/query"; +import { makeSchemaCommand } from "./commands/schema"; +import { makeScriptCommand } from "./commands/script"; +import { makeWebhookCommand } from "./commands/webhook"; +import { handleCliError } from "./utils/errors"; +import { ENV_NAMES } from "./utils/connection"; + +const program = new Command(); + +program + .name("fmodata") + .description("FileMaker OData CLI — query, script, webhook, metadata, and schema operations") + .version("0.1.0") + .option("--server ", `FM server URL [env: ${ENV_NAMES.server}]`) + .option("--database ", `FM database name [env: ${ENV_NAMES.db}]`) + .option("--username ", `FM username [env: ${ENV_NAMES.username}]`) + .option("--password ", `FM password [env: ${ENV_NAMES.password}]`) + .option("--api-key ", `OttoFMS API key [env: ${ENV_NAMES.apiKey}]`) + .option("--table", "Output as table (default: JSON)", false); + +program.addCommand(makeQueryCommand()); +program.addCommand(makeScriptCommand()); +program.addCommand(makeWebhookCommand()); +program.addCommand(makeMetadataCommand()); +program.addCommand(makeSchemaCommand()); + +program.exitOverride(); + +try { + await program.parseAsync(process.argv); +} catch (err) { + // Commander throws CommanderError for --help/--version exits (non-zero but expected) + if (err && typeof err === "object" && "code" in err) { + const code = (err as { code: string }).code; + if (code === "commander.helpDisplayed" || code === "commander.version") { + process.exit(0); + } + } + handleCliError(err); +} diff --git a/packages/fmodata/src/cli/utils/connection.ts b/packages/fmodata/src/cli/utils/connection.ts new file mode 100644 index 0000000..7c79623 --- /dev/null +++ b/packages/fmodata/src/cli/utils/connection.ts @@ -0,0 +1,51 @@ +import { Database } from "../../client/database"; +import { FMServerConnection } from "../../client/filemaker-odata"; + +export const ENV_NAMES = { + server: "FM_SERVER", + db: "FM_DATABASE", + username: "FM_USERNAME", + password: "FM_PASSWORD", + apiKey: "OTTO_API_KEY", +} as const; + +export interface ConnectionOptions { + server?: string; + database?: string; + username?: string; + password?: string; + apiKey?: string; +} + +export interface BuiltConnection { + connection: FMServerConnection; + db: Database; +} + +export function buildConnection(opts: ConnectionOptions): BuiltConnection { + const server = opts.server ?? process.env[ENV_NAMES.server]; + const database = opts.database ?? process.env[ENV_NAMES.db]; + const apiKey = opts.apiKey ?? process.env[ENV_NAMES.apiKey]; + const username = opts.username ?? process.env[ENV_NAMES.username]; + const password = opts.password ?? process.env[ENV_NAMES.password]; + + if (!server) { + throw new Error(`Missing required: --server or ${ENV_NAMES.server} environment variable`); + } + if (!database) { + throw new Error(`Missing required: --database or ${ENV_NAMES.db} environment variable`); + } + if (!apiKey && !username) { + throw new Error( + `Missing required auth: --api-key (${ENV_NAMES.apiKey}) or --username (${ENV_NAMES.username})`, + ); + } + if (username && !password) { + throw new Error(`Missing required: --password (${ENV_NAMES.password}) when using username auth`); + } + + const auth = apiKey ? { apiKey } : { username: username as string, password: password as string }; + const connection = new FMServerConnection({ serverUrl: server, auth }); + const db = connection.database(database); + return { connection, db }; +} diff --git a/packages/fmodata/src/cli/utils/errors.ts b/packages/fmodata/src/cli/utils/errors.ts new file mode 100644 index 0000000..13b903e --- /dev/null +++ b/packages/fmodata/src/cli/utils/errors.ts @@ -0,0 +1,5 @@ +export function handleCliError(err: unknown): never { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write(`Error: ${message}\n`); + process.exit(1); +} diff --git a/packages/fmodata/src/cli/utils/output.ts b/packages/fmodata/src/cli/utils/output.ts new file mode 100644 index 0000000..61306ff --- /dev/null +++ b/packages/fmodata/src/cli/utils/output.ts @@ -0,0 +1,45 @@ +import Table from "cli-table3"; + +export interface OutputOptions { + table: boolean; +} + +export function printResult(data: unknown, opts: OutputOptions): void { + if (opts.table) { + printTable(data); + } else { + console.log(JSON.stringify(data, null, 2)); + } +} + +function printTable(data: unknown): void { + // Array of objects — render as rows + if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null) { + const keys = Object.keys(data[0] as Record); + const table = new Table({ head: keys }); + for (const row of data) { + table.push(keys.map((k) => String((row as Record)[k] ?? ""))); + } + console.log(table.toString()); + return; + } + + // Single object — render as key-value pairs + if (typeof data === "object" && data !== null && !Array.isArray(data)) { + const table = new Table({ head: ["Key", "Value"] }); + for (const [key, value] of Object.entries(data as Record)) { + table.push([key, typeof value === "object" ? JSON.stringify(value) : String(value ?? "")]); + } + console.log(table.toString()); + return; + } + + // Fallback — just print as JSON + console.log(JSON.stringify(data, null, 2)); +} + +export function printError(err: unknown): void { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write(`Error: ${message}\n`); + process.exit(1); +} diff --git a/packages/fmodata/tests/cli/commands/metadata.test.ts b/packages/fmodata/tests/cli/commands/metadata.test.ts new file mode 100644 index 0000000..09b993f --- /dev/null +++ b/packages/fmodata/tests/cli/commands/metadata.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { FMServerConnection } from "../../../src/client/filemaker-odata"; +import { simpleMock } from "../../utils/mock-fetch"; + +function createTestDb(mockFetch?: typeof fetch) { + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + ...(mockFetch ? { fetchClientOptions: { fetchHandler: mockFetch } } : {}), + }); + return connection.database("TestDB.fmp12"); +} + +describe("metadata commands (unit)", () => { + it("listTableNames method exists", () => { + const db = createTestDb(); + expect(typeof db.listTableNames).toBe("function"); + }); + + it("listTableNames parses value array correctly via raw request", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { value: [{ name: "contacts" }, { name: "users" }] }, + }); + + const result = await db._makeRequest<{ value: Array<{ name: string }> }>("/", { + fetchHandler: mockFetch, + }); + expect(result.error).toBeUndefined(); + expect(result.data?.value).toHaveLength(2); + expect(result.data?.value?.[0]?.name).toBe("contacts"); + }); + + it("listTableNames returns string array", async () => { + const mockFetch = simpleMock({ + status: 200, + body: { value: [{ name: "contacts" }, { name: "users" }] }, + }); + const db = createTestDb(mockFetch); + const tables = await db.listTableNames(); + expect(tables).toEqual(["contacts", "users"]); + }); + + it("getMetadata (json) returns metadata object via raw request", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { "TestDB.fmp12": { entityContainer: { entitySets: [] } } }, + }); + + const result = await db._makeRequest>("/$metadata", { + fetchHandler: mockFetch, + headers: { Accept: "application/json" }, + }); + expect(result.error).toBeUndefined(); + expect(result.data).toHaveProperty("TestDB.fmp12"); + }); +}); diff --git a/packages/fmodata/tests/cli/commands/query.test.ts b/packages/fmodata/tests/cli/commands/query.test.ts new file mode 100644 index 0000000..04e3e2a --- /dev/null +++ b/packages/fmodata/tests/cli/commands/query.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { FMServerConnection } from "../../../src/client/filemaker-odata"; +import { simpleMock } from "../../utils/mock-fetch"; + +function createTestDb() { + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + }); + return connection.database("TestDB.fmp12"); +} + +describe("query commands (unit — raw OData requests)", () => { + it("list builds correct URL with $top and $filter", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { value: [{ name: "Alice" }] }, + }); + + const result = await db._makeRequest<{ value: unknown[] }>("/contacts?$top=5&$filter=name eq 'Alice'", { + fetchHandler: mockFetch, + }); + expect(result.error).toBeUndefined(); + expect(result.data?.value).toHaveLength(1); + }); + + it("insert sends POST request", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 201, + body: { name: "Bob", id: "1" }, + }); + + const result = await db._makeRequest("/contacts", { + method: "POST", + body: JSON.stringify({ name: "Bob" }), + fetchHandler: mockFetch, + }); + expect(result.error).toBeUndefined(); + }); + + it("update sends PATCH request", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { updated: 1 }, + }); + + const result = await db._makeRequest("/contacts?$filter=name eq 'Bob'", { + method: "PATCH", + body: JSON.stringify({ name: "Robert" }), + fetchHandler: mockFetch, + }); + expect(result.error).toBeUndefined(); + }); + + it("delete sends DELETE request", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: null, + headers: { "fmodata.affected_rows": "1" }, + }); + + const result = await db._makeRequest("/contacts?$filter=name eq 'Bob'", { + method: "DELETE", + fetchHandler: mockFetch, + }); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(1); + }); +}); diff --git a/packages/fmodata/tests/cli/commands/schema.test.ts b/packages/fmodata/tests/cli/commands/schema.test.ts new file mode 100644 index 0000000..1930a5e --- /dev/null +++ b/packages/fmodata/tests/cli/commands/schema.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { FMServerConnection } from "../../../src/client/filemaker-odata"; +import { simpleMock } from "../../utils/mock-fetch"; + +function createTestDb() { + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + }); + return connection.database("TestDB.fmp12"); +} + +describe("schema commands (unit)", () => { + it("createTable sends POST to FileMaker_Tables", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { tableName: "NewTable", fields: [] }, + }); + + const result = await db.schema.createTable( + "NewTable", + [{ name: "Name", type: "string" }], + { fetchHandler: mockFetch }, + ); + expect(result.tableName).toBe("NewTable"); + }); + + it("addFields sends PATCH to FileMaker_Tables/tableName", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { tableName: "contacts", fields: [{ name: "Notes", type: "varchar" }] }, + }); + + const result = await db.schema.addFields( + "contacts", + [{ name: "Notes", type: "string" }], + { fetchHandler: mockFetch }, + ); + expect(result.tableName).toBe("contacts"); + expect(result.fields).toHaveLength(1); + }); + + it("listTableNames (used for schema list-tables) returns table names", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { value: [{ name: "contacts" }, { name: "users" }] }, + }); + + const result = await db._makeRequest<{ value: Array<{ name: string }> }>("/", { + fetchHandler: mockFetch, + }); + const names = result.data?.value?.map((t) => t.name) ?? []; + expect(names).toEqual(["contacts", "users"]); + }); +}); diff --git a/packages/fmodata/tests/cli/commands/script.test.ts b/packages/fmodata/tests/cli/commands/script.test.ts new file mode 100644 index 0000000..63edaa1 --- /dev/null +++ b/packages/fmodata/tests/cli/commands/script.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { FMServerConnection } from "../../../src/client/filemaker-odata"; +import { simpleMock } from "../../utils/mock-fetch"; + +describe("script run command (unit)", () => { + it("runs script without param", async () => { + const mockFetch = simpleMock({ + status: 200, + body: { scriptResult: { code: 0, resultParameter: "done" } }, + }); + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + fetchClientOptions: { fetchHandler: mockFetch }, + }); + const db = connection.database("TestDB.fmp12"); + + const result = await db.runScript("MyScript"); + expect(result.resultCode).toBe(0); + expect(result.result).toBe("done"); + }); + + it("runs script with string param", async () => { + const mockFetch = simpleMock({ + status: 200, + body: { scriptResult: { code: 0, resultParameter: "ok" } }, + }); + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + fetchClientOptions: { fetchHandler: mockFetch }, + }); + const db = connection.database("TestDB.fmp12"); + + const result = await db.runScript("MyScript", { scriptParam: "hello" }); + expect(result.resultCode).toBe(0); + }); + + it("runs script with object param", async () => { + const mockFetch = simpleMock({ + status: 200, + body: { scriptResult: { code: 0, resultParameter: "ok" } }, + }); + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + fetchClientOptions: { fetchHandler: mockFetch }, + }); + const db = connection.database("TestDB.fmp12"); + + const result = await db.runScript("MyScript", { scriptParam: { key: "value" } }); + expect(result.resultCode).toBe(0); + }); +}); diff --git a/packages/fmodata/tests/cli/commands/webhook.test.ts b/packages/fmodata/tests/cli/commands/webhook.test.ts new file mode 100644 index 0000000..185f804 --- /dev/null +++ b/packages/fmodata/tests/cli/commands/webhook.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { FMServerConnection } from "../../../src/client/filemaker-odata"; +import { simpleMock } from "../../utils/mock-fetch"; + +function createTestDb() { + const connection = new FMServerConnection({ + serverUrl: "https://api.example.com", + auth: { apiKey: "test-key" }, + }); + return connection.database("TestDB.fmp12"); +} + +describe("webhook commands (unit)", () => { + it("list returns webhook list response", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { Status: "OK", WebHook: [{ webHookID: 1, tableName: "contacts", url: "https://example.com" }] }, + }); + + const result = await db.webhook.list({ fetchHandler: mockFetch }); + expect(result.Status).toBe("OK"); + expect(result.WebHook).toHaveLength(1); + expect(result.WebHook[0]?.webHookID).toBe(1); + }); + + it("get returns webhook by id", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { webHookID: 42, tableName: "contacts", url: "https://example.com", notifySchemaChanges: false, select: "", filter: "", pendingOperations: [] }, + }); + + const result = await db.webhook.get(42, { fetchHandler: mockFetch }); + expect(result.webHookID).toBe(42); + }); + + it("remove calls delete endpoint", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ status: 204 }); + + await expect(db.webhook.remove(1, { fetchHandler: mockFetch })).resolves.toBeUndefined(); + }); + + it("add creates webhook with string tableName via proxy", async () => { + const db = createTestDb(); + const mockFetch = simpleMock({ + status: 200, + body: { webHookResult: { webHookID: 99 } }, + }); + + // Test the table proxy approach used in the CLI + const tableProxy = { + [Symbol.for("fmodata:FMTableName")]: "contacts", + } as unknown as import("../../../src/orm/table").FMTable, string>; + + const result = await db.webhook.add( + { + webhook: "https://example.com/hook", + tableName: tableProxy, + }, + { fetchHandler: mockFetch }, + ); + expect(result.webHookResult.webHookID).toBe(99); + }); +}); diff --git a/packages/fmodata/tests/cli/integration/binary.test.ts b/packages/fmodata/tests/cli/integration/binary.test.ts new file mode 100644 index 0000000..0f3a11c --- /dev/null +++ b/packages/fmodata/tests/cli/integration/binary.test.ts @@ -0,0 +1,50 @@ +import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const CLI_PATH = resolve(__dirname, "../../../dist/cli/index.js"); + +describe("CLI binary integration", () => { + it("dist/cli/index.js exists after build", () => { + // This test verifies the binary was built. + // If the file doesn't exist, skip with a hint rather than failing hard. + if (!existsSync(CLI_PATH)) { + console.warn("CLI binary not found at", CLI_PATH, "— run `pnpm build` first"); + return; + } + expect(existsSync(CLI_PATH)).toBe(true); + }); + + it("fmodata --help exits 0 and shows usage", () => { + if (!existsSync(CLI_PATH)) { + console.warn("Skipping: CLI binary not built"); + return; + } + const output = execSync(`node ${CLI_PATH} --help`, { encoding: "utf8" }); + expect(output).toContain("fmodata"); + expect(output).toContain("Usage"); + }); + + it("fmodata query --help shows query subcommands", () => { + if (!existsSync(CLI_PATH)) { + console.warn("Skipping: CLI binary not built"); + return; + } + const output = execSync(`node ${CLI_PATH} query --help`, { encoding: "utf8" }); + expect(output).toContain("list"); + expect(output).toContain("insert"); + expect(output).toContain("update"); + expect(output).toContain("delete"); + }); + + it("fmodata metadata --help shows metadata subcommands", () => { + if (!existsSync(CLI_PATH)) { + console.warn("Skipping: CLI binary not built"); + return; + } + const output = execSync(`node ${CLI_PATH} metadata --help`, { encoding: "utf8" }); + expect(output).toContain("get"); + expect(output).toContain("tables"); + }); +}); diff --git a/packages/fmodata/tests/cli/unit/connection.test.ts b/packages/fmodata/tests/cli/unit/connection.test.ts new file mode 100644 index 0000000..4b7e7d6 --- /dev/null +++ b/packages/fmodata/tests/cli/unit/connection.test.ts @@ -0,0 +1,94 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { buildConnection, ENV_NAMES } from "../../../src/cli/utils/connection"; + +describe("buildConnection", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("builds a connection from env vars with api key auth", () => { + process.env[ENV_NAMES.server] = "https://example.com"; + process.env[ENV_NAMES.db] = "MyDB.fmp12"; + process.env[ENV_NAMES.apiKey] = "test-key"; + + const { connection, db } = buildConnection({}); + expect(connection).toBeDefined(); + expect(db).toBeDefined(); + }); + + it("builds a connection from env vars with username/password auth", () => { + process.env[ENV_NAMES.server] = "https://example.com"; + process.env[ENV_NAMES.db] = "MyDB.fmp12"; + process.env[ENV_NAMES.username] = "admin"; + process.env[ENV_NAMES.password] = "secret"; + + const { connection, db } = buildConnection({}); + expect(connection).toBeDefined(); + expect(db).toBeDefined(); + }); + + it("CLI options override env vars", () => { + process.env[ENV_NAMES.server] = "https://env-server.com"; + process.env[ENV_NAMES.db] = "EnvDB.fmp12"; + process.env[ENV_NAMES.apiKey] = "env-key"; + + const { db } = buildConnection({ + server: "https://cli-server.com", + database: "CliDB.fmp12", + apiKey: "cli-key", + }); + expect(db).toBeDefined(); + }); + + it("throws when server is missing", () => { + process.env[ENV_NAMES.db] = "MyDB.fmp12"; + process.env[ENV_NAMES.apiKey] = "test-key"; + delete process.env[ENV_NAMES.server]; + + expect(() => buildConnection({})).toThrow(/server/i); + }); + + it("throws when database is missing", () => { + process.env[ENV_NAMES.server] = "https://example.com"; + process.env[ENV_NAMES.apiKey] = "test-key"; + delete process.env[ENV_NAMES.db]; + + expect(() => buildConnection({})).toThrow(/database/i); + }); + + it("throws when auth is missing", () => { + process.env[ENV_NAMES.server] = "https://example.com"; + process.env[ENV_NAMES.db] = "MyDB.fmp12"; + delete process.env[ENV_NAMES.apiKey]; + delete process.env[ENV_NAMES.username]; + delete process.env[ENV_NAMES.password]; + + expect(() => buildConnection({})).toThrow(/auth/i); + }); + + it("throws when username is set but password is missing", () => { + process.env[ENV_NAMES.server] = "https://example.com"; + process.env[ENV_NAMES.db] = "MyDB.fmp12"; + process.env[ENV_NAMES.username] = "admin"; + delete process.env[ENV_NAMES.password]; + + expect(() => buildConnection({})).toThrow(/password/i); + }); + + it("prefers api key over username auth when both are set", () => { + const { db } = buildConnection({ + server: "https://example.com", + database: "MyDB.fmp12", + apiKey: "api-key", + username: "admin", + password: "secret", + }); + expect(db).toBeDefined(); + }); +}); diff --git a/packages/fmodata/tests/cli/unit/output.test.ts b/packages/fmodata/tests/cli/unit/output.test.ts new file mode 100644 index 0000000..7f43b99 --- /dev/null +++ b/packages/fmodata/tests/cli/unit/output.test.ts @@ -0,0 +1,65 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { printError, printResult } from "../../../src/cli/utils/output"; + +describe("printResult", () => { + let stdoutSpy: ReturnType; + + beforeEach(() => { + stdoutSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + it("prints JSON by default", () => { + const data = [{ name: "Alice", age: 30 }]; + printResult(data, { table: false }); + expect(stdoutSpy).toHaveBeenCalledWith(JSON.stringify(data, null, 2)); + }); + + it("prints table for array of objects", () => { + const data = [ + { name: "Alice", age: 30 }, + { name: "Bob", age: 25 }, + ]; + printResult(data, { table: true }); + const output = stdoutSpy.mock.calls[0]?.[0] as string; + expect(output).toContain("name"); + expect(output).toContain("age"); + expect(output).toContain("Alice"); + expect(output).toContain("Bob"); + }); + + it("prints key-value table for single object", () => { + const data = { status: "ok", count: 42 }; + printResult(data, { table: true }); + const output = stdoutSpy.mock.calls[0]?.[0] as string; + expect(output).toContain("Key"); + expect(output).toContain("Value"); + expect(output).toContain("status"); + expect(output).toContain("ok"); + }); + + it("falls back to JSON for non-object data in table mode", () => { + printResult(["a", "b", "c"], { table: true }); + const output = stdoutSpy.mock.calls[0]?.[0] as string; + expect(output).toContain('"a"'); + }); +}); + +describe("printError", () => { + it("writes to stderr and exits", () => { + const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + + expect(() => printError(new Error("something went wrong"))).toThrow("process.exit called"); + expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("something went wrong")); + expect(exitSpy).toHaveBeenCalledWith(1); + + stderrSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); diff --git a/packages/fmodata/tsdown.config.ts b/packages/fmodata/tsdown.config.ts new file mode 100644 index 0000000..d8afda2 --- /dev/null +++ b/packages/fmodata/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/cli/index.ts"], + format: ["esm"], + target: "esnext", + outDir: "dist/cli", + clean: false, + banner: { + js: "#!/usr/bin/env node", + }, +}); diff --git a/packages/fmodata/vitest.config.ts b/packages/fmodata/vitest.config.ts index ee5ce02..d0ce52d 100644 --- a/packages/fmodata/vitest.config.ts +++ b/packages/fmodata/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ // When you pass a file path directly (e.g., vitest run tests/e2e.test.ts), // vitest will run it regardless of the exclude pattern // Run E2E tests with: pnpm test:e2e - exclude: ["**/node_modules/**", "**/dist/**", "tests/e2e/**"], + exclude: ["**/node_modules/**", "**/dist/**", "tests/e2e/**", "tests/cli/e2e/**"], typecheck: { enabled: true, include: ["src/**/*.ts", "tests/**/*.test.ts", "tests/**/*.test-d.ts"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e577200..f4af01d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -365,7 +365,7 @@ importers: version: 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) '@trpc/next': specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@trpc/react-query': specifier: 11.0.0-rc.441 version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -410,7 +410,7 @@ importers: version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) postgres: specifier: ^3.4.8 version: 3.4.8 @@ -517,6 +517,9 @@ importers: '@fetchkit/ffetch': specifier: ^4.2.0 version: 4.2.0 + commander: + specifier: ^14.0.2 + version: 14.0.2 dotenv: specifier: ^16.6.1 version: 16.6.1 @@ -539,12 +542,18 @@ importers: '@types/node': specifier: ^22.19.5 version: 22.19.5 + cli-table3: + specifier: ^0.6.5 + version: 0.6.5 fast-xml-parser: specifier: ^5.3.3 version: 5.3.3 publint: specifier: ^0.3.16 version: 0.3.16 + tsdown: + specifier: ^0.14.2 + version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -2381,8 +2390,8 @@ packages: resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} - '@oxc-project/types@0.111.0': - resolution: {integrity: sha512-bh54LJMafgRGl2cPQ/QM+tI5rWaShm/wK9KywEj/w36MhiPKXYM67H2y3q+9pr4YO7ufwg2AKdBAZkhHBD8ClA==} + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} '@oxc-resolver/binding-android-arm-eabi@11.16.2': resolution: {integrity: sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==} @@ -3303,79 +3312,91 @@ packages: peerDependencies: react: '>=18.2.0' - '@rolldown/binding-android-arm64@1.0.0-rc.2': - resolution: {integrity: sha512-AGV80viZ4Hil4C16GFH+PSwq10jclV9oyRFhD+5HdowPOCJ+G+99N5AClQvMkUMIahTY8cX0SQpKEEWcCg6fSA==} + '@rolldown/binding-android-arm64@1.0.0-rc.8': + resolution: {integrity: sha512-5bcmMQDWEfWUq3m79Mcf/kbO6e5Jr6YjKSsA1RnpXR6k73hQ9z1B17+4h93jXpzHvS18p7bQHM1HN/fSd+9zog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.2': - resolution: {integrity: sha512-PYR+PQu1mMmQiiKHN2JiOctvH32Xc/Mf+Su2RSmWtC9BbIqlqsVWjbulnShk0imjRim0IsbkMMCN5vYQwiuqaA==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.8': + resolution: {integrity: sha512-dcHPd5N4g9w2iiPRJmAvO0fsIWzF2JPr9oSuTjxLL56qu+oML5aMbBMNwWbk58Mt3pc7vYs9CCScwLxdXPdRsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.2': - resolution: {integrity: sha512-X2G36Z6oh5ynoYpE2JAyG+uQ4kO/3N7XydM/I98FNk8VVgDKjajFF+v7TXJ2FMq6xa7Xm0UIUKHW2MRQroqoUA==} + '@rolldown/binding-darwin-x64@1.0.0-rc.8': + resolution: {integrity: sha512-mw0VzDvoj8AuR761QwpdCFN0sc/jspuc7eRYJetpLWd+XyansUrH3C7IgNw6swBOgQT9zBHNKsVCjzpfGJlhUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.2': - resolution: {integrity: sha512-XpiFTsl9qjiDfrmJF6CE3dgj1nmSbxUIT+p2HIbXV6WOj/32btO8FKkWSsOphUwVinEt3R8HVkVrcLtFNruMMQ==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.8': + resolution: {integrity: sha512-xNrRa6mQ9NmMIJBdJtPMPG8Mso0OhM526pDzc/EKnRrIrrkHD1E0Z6tONZRmUeJElfsQ6h44lQQCcDilSNIvSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.2': - resolution: {integrity: sha512-zjYZ99e47Wlygs4hW+sQ+kshlO8ake9OoY2ecnJ9cwpDGiiIB9rQ3LgP3kt8j6IeVyMSksu//VEhc8Mrd1lRIw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8': + resolution: {integrity: sha512-WgCKoO6O/rRUwimWfEJDeztwJJmuuX0N2bYLLRxmXDTtCwjToTOqk7Pashl/QpQn3H/jHjx0b5yCMbcTVYVpNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.2': - resolution: {integrity: sha512-Piso04EZ9IHV1aZSsLQVMOPTiCq4Ps2UPL3pchjNXHGJGFiB9U42s22LubPaEBFS+i6tCawS5EarIwex1zC4BA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8': + resolution: {integrity: sha512-tOHgTOQa8G4Z3ULj4G3NYOGGJEsqPHR91dT72u63OtVsZ7B6wFJKOx+ZKv+pvwzxWz92/I2ycaqi2/Ll4l+rlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.2': - resolution: {integrity: sha512-OwJCeMZlmjKsN9pfJfTmqYpe3JC+L6RO87+hu9ajRLr1Lh6cM2FRQ8e48DLRyRDww8Ti695XQvqEANEMmsuzLw==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.8': + resolution: {integrity: sha512-oRbxcgDujCi2Yp1GTxoUFsIFlZsuPHU4OV4AzNc3/6aUmR4lfm9FK0uwQu82PJsuUwnF2jFdop3Ep5c1uK7Uxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.2': - resolution: {integrity: sha512-uQqBmA8dTWbKvfqbeSsXNUssRGfdgQCc0hkGfhQN7Pf85wG2h0Fd/z2d+ykyT4YbcsjQdgEGxBNsg3v4ekOuEA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8': + resolution: {integrity: sha512-oaLRyUHw8kQE5M89RqrDJZ10GdmGJcMeCo8tvaE4ukOofqgjV84AbqBSH6tTPjeT2BHv+xlKj678GBuIb47lKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8': + resolution: {integrity: sha512-1hjSKFrod5MwBBdLOOA0zpUuSfSDkYIY+QqcMcIU1WOtswZtZdUkcFcZza9b2HcAb0bnpmmyo0LZcaxLb2ov1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.8': + resolution: {integrity: sha512-a1+F0aV4Wy9tT3o+cHl3XhOy6aFV+B8Ll+/JFj98oGkb6lGk3BNgrxd+80RwYRVd23oLGvj3LwluKYzlv1PEuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.2': - resolution: {integrity: sha512-ItZabVsICCYWHbP+jcAgNzjPAYg5GIVQp/NpqT6iOgWctaMYtobClc5m0kNtxwqfNrLXoyt998xUey4AvcxnGQ==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.8': + resolution: {integrity: sha512-bGyXCFU11seFrf7z8PcHSwGEiFVkZ9vs+auLacVOQrVsI8PFHJzzJROF3P6b0ODDmXr0m6Tj5FlDhcXVk0Jp8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.2': - resolution: {integrity: sha512-U4UYANwafcMXSUC0VqdrqTAgCo2v8T7SiuTYwVFXgia0KOl8jiv3okwCFqeZNuw/G6EWDiqhT8kK1DLgyLsxow==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.8': + resolution: {integrity: sha512-n8d+L2bKgf9G3+AM0bhHFWdlz9vYKNim39ujRTieukdRek0RAo2TfG2uEnV9spa4r4oHUfL9IjcY3M9SlqN1gw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.2': - resolution: {integrity: sha512-ZIWCjQsMon4tqRoao0Vzowjwx0cmFT3kublh2nNlgeasIJMWlIGHtr0d4fPypm57Rqx4o1h4L8SweoK2q6sMGA==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.8': + resolution: {integrity: sha512-4R4iJDIk7BrJdteAbEAICXPoA7vZoY/M0OBfcRlQxzQvUYMcEp2GbC/C8UOgQJhu2TjGTpX1H8vVO1xHWcRqQA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.2': - resolution: {integrity: sha512-NIo7vwRUPEzZ4MuZGr5YbDdjJ84xdiG+YYf8ZBfTgvIsk9wM0sZamJPEXvaLkzVIHpOw5uqEHXS85Gqqb7aaqQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8': + resolution: {integrity: sha512-3lwnklba9qQOpFnQ7EW+A1m4bZTWXZE4jtehsZ0YOl2ivW1FQqp5gY7X2DLuKITggesyuLwcmqS11fA7NtrmrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.2': - resolution: {integrity: sha512-bLKzyLFbvngeNPZocuLo3LILrKwCrkyMxmRXs6fZYDrvh7cyZRw9v56maDL9ipPas0OOmQK1kAKYwvTs30G21Q==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.8': + resolution: {integrity: sha512-VGjCx9Ha1P/r3tXGDZyG0Fcq7Q0Afnk64aaKzr1m40vbn1FL8R3W0V1ELDvPgzLXaaqK/9PnsqSaLWXfn6JtGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3383,8 +3404,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + '@rolldown/pluginutils@1.0.0-rc.8': + resolution: {integrity: sha512-wzJwL82/arVfeSP3BLr1oTy40XddjtEdrdgtJ4lLRBu06mP3q/8HGM6K0JRlQuTA3XB0pNJx2so/nmpY4xyOew==} '@rollup/plugin-replace@6.0.3': resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} @@ -6691,6 +6712,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prettier@2.8.8: @@ -6986,8 +7008,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-rc.2: - resolution: {integrity: sha512-1g/8Us9J8sgJGn3hZfBecX1z4U3y5KO7V/aV2U1M/9UUzLNqHA8RfFQ/NPT7HLxOIldyIgrcjaYTRvA81KhJIg==} + rolldown@1.0.0-rc.8: + resolution: {integrity: sha512-RGOL7mz/aoQpy/y+/XS9iePBfeNRDUdozrhCEJxdpJyimW8v6yp4c30q6OviUU5AnUJVLRL9GP//HUs6N3ALrQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -9476,7 +9498,7 @@ snapshots: '@orama/orama@3.1.18': {} - '@oxc-project/types@0.111.0': {} + '@oxc-project/types@0.115.0': {} '@oxc-resolver/binding-android-arm-eabi@11.16.2': optional: true @@ -10386,50 +10408,56 @@ snapshots: dependencies: react: 19.2.3 - '@rolldown/binding-android-arm64@1.0.0-rc.2': + '@rolldown/binding-android-arm64@1.0.0-rc.8': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.8': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.8': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.2': + '@rolldown/binding-freebsd-x64@1.0.0-rc.8': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.2': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.2': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.2': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.8': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.2': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.2': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.2': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.8': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.2': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.8': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.2': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.8': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.2': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.8': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.2': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.2': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.8': optional: true '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.8': {} '@rollup/plugin-replace@6.0.3(rollup@4.55.1)': dependencies: @@ -10859,7 +10887,7 @@ snapshots: dependencies: '@trpc/server': 11.0.0-rc.441 - '@trpc/next@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@trpc/next@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@trpc/client': 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) '@trpc/server': 11.0.0-rc.441 @@ -13884,7 +13912,7 @@ snapshots: optionalDependencies: '@rollup/rollup-linux-x64-gnu': 4.55.1 - next-auth@4.24.13(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-auth@4.24.13(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1 @@ -14630,7 +14658,7 @@ snapshots: rfdc@1.4.1: {} - rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.2)(typescript@5.9.3): + rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.8)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -14640,31 +14668,33 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) dts-resolver: 2.1.3(oxc-resolver@11.16.2) get-tsconfig: 4.13.0 - rolldown: 1.0.0-rc.2 + rolldown: 1.0.0-rc.8 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-rc.2: - dependencies: - '@oxc-project/types': 0.111.0 - '@rolldown/pluginutils': 1.0.0-rc.2 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.2 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.2 - '@rolldown/binding-darwin-x64': 1.0.0-rc.2 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.2 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.2 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.2 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.2 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.2 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.2 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.2 + rolldown@1.0.0-rc.8: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.8 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.8 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.8 + '@rolldown/binding-darwin-x64': 1.0.0-rc.8 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.8 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.8 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.8 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.8 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.8 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.8 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.8 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.8 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.8 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.8 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.8 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.8 rollup-plugin-preserve-directives@0.4.0(rollup@4.55.1): dependencies: @@ -15244,8 +15274,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-rc.2 - rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.2)(typescript@5.9.3) + rolldown: 1.0.0-rc.8 + rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.0-rc.8)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15