diff --git a/bun.lock b/bun.lock index 281da17..7393077 100644 --- a/bun.lock +++ b/bun.lock @@ -20,9 +20,10 @@ "backlink-cli": "bin/backlink-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0", }, @@ -42,9 +43,10 @@ "google-ads-cli": "bin/google-ads-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0", }, @@ -64,9 +66,10 @@ "gsc-cli": "bin/gsc-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "googleapis": "^166.0.0", "valibot": "^1.2.0", @@ -87,9 +90,10 @@ "page-extract-cli": "bin/page-extract-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "valibot": "^1.2.0", }, "devDependencies": { @@ -108,10 +112,11 @@ "posthog-cli": "bin/posthog-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@posthog/agent-toolkit": "^0.2.4", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0", }, @@ -131,9 +136,10 @@ "serp-snapshot-cli": "bin/serp-snapshot-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0", }, @@ -153,9 +159,10 @@ "sitemap-watch-cli": "bin/sitemap-watch-cli.js", }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "fast-xml-parser": "^5.3.0", "valibot": "^1.2.0", }, @@ -172,7 +179,7 @@ "@types/bun": "^1.3.10", "@types/node": "^25.6.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "bun-types": "^1.3.13", "dotenv": "^17.2.2", "typescript": "^5", @@ -200,6 +207,8 @@ "@deniffer/backlink-cli": ["@deniffer/backlink-cli@workspace:packages/backlink-cli"], + "@deniffer/cli-kit": ["@deniffer/cli-kit@github:Deniffer001/cli-kit#057d30b", { "dependencies": { "dotenv": "^17.4.2" }, "peerDependencies": { "argc": "*", "valibot": "^1" } }, "Deniffer001-cli-kit-057d30b", "sha512-oodW8Z+TpNfSL14uSCjGX7LsVB2av9Tm5EEkwnIE8Gr26NbuCaFl5aMLl7+MaWX/JzDQtAg810ilwJ8DaxXkSA=="], + "@deniffer/google-ads-cli": ["@deniffer/google-ads-cli@workspace:packages/google-ads-cli"], "@deniffer/gsc-cli": ["@deniffer/gsc-cli@workspace:packages/gsc-cli"], @@ -758,17 +767,7 @@ "@ai-sdk/ui-utils/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - "@deniffer/google-ads-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], - - "@deniffer/gsc-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], - - "@deniffer/page-extract-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], - - "@deniffer/posthog-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], - - "@deniffer/serp-snapshot-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], - - "@deniffer/sitemap-watch-cli/argc": ["argc@github:ethan-huo/argc#7d7b60d", { "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-7d7b60d", "sha512-eFUGCZnU2eJoSPzbs/YEJuXOp/K+BxZna2ki9+jXhvdoDbyTyGsFfINY5pULcuahl8PSqKRULNsCutq/f2rPpg=="], + "@deniffer/cli-kit/argc": ["argc@0.1.1", "", {}, "sha512-Nv1PUrvwahIZUAHk3ZB5zZqEaKYKL76DDX8DqZOZMtNnuWDlSxPmQ5mSVGS/w/Puiib/nEHKAC3h1tpe2QrneA=="], "@langchain/core/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], diff --git a/package.json b/package.json index 3e1656a..73e7b43 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@types/bun": "^1.3.10", "@types/node": "^25.6.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "bun-types": "^1.3.13", "dotenv": "^17.2.2", "typescript": "^5", diff --git a/packages/backlink-cli/lib/errors.ts b/packages/backlink-cli/lib/errors.ts index 637ab5f..263b97d 100644 --- a/packages/backlink-cli/lib/errors.ts +++ b/packages/backlink-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified backlink provider failures - * @output stable agent-facing error objects - * @pos shared error contract for backlink handlers and provider calls + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos backlink error contract = cli-kit core + a backlink-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "auth_error" @@ -13,36 +22,13 @@ export type CliErrorCode = | "parse_error" | "backend_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - function readStatus(error: Error) { const status = Reflect.get(error, "status"); return typeof status === "number" ? status : null; } -export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - +// The backlink-specific divergence, plugged into cli-kit's normalizer seam. +export const backlinkErrorMapper: CliErrorMapper = (error) => { if (error instanceof TypeError) { return cliError({ code: "network_error", @@ -51,44 +37,40 @@ export function normalizeCliError(error: unknown) { }); } - if (error instanceof Error) { - const status = readStatus(error); - - if (status === 401 || status === 403) { - return cliError({ - code: "auth_error", - message: error.message, - hint: "Check backlink provider credentials.", - }); - } - - if (status === 402 || status === 429) { - return cliError({ - code: "quota_error", - message: error.message, - hint: "Check backlink provider credits, subscription, rate limits, and retry policy.", - }); - } - - if (status !== null) { - return cliError({ - code: "provider_error", - message: error.message, - hint: - status >= 500 - ? "The backlink provider returned a server error. Retry later." - : "The backlink provider rejected the request. Check input fields.", - }); - } + if (!(error instanceof Error)) { + return null; + } + const status = readStatus(error); + if (status === 401 || status === 403) { + return cliError({ + code: "auth_error", + message: error.message, + hint: "Check backlink provider credentials.", + }); + } + if (status === 402 || status === 429) { return cliError({ - code: "backend_failure", + code: "quota_error", message: error.message, + hint: "Check backlink provider credits, subscription, rate limits, and retry policy.", }); } + if (status !== null) { + return cliError({ + code: "provider_error", + message: error.message, + hint: + status >= 500 + ? "The backlink provider returned a server error. Retry later." + : "The backlink provider rejected the request. Check input fields.", + }); + } + + // Defer Error-without-status and non-Error to cli-kit's backend_failure fallback. + return null; +}; - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); +export function normalizeCliError(error: unknown) { + return normalizeWithMappers(error, [backlinkErrorMapper]); } diff --git a/packages/backlink-cli/output.ts b/packages/backlink-cli/output.ts index 837f598..b631d11 100644 --- a/packages/backlink-cli/output.ts +++ b/packages/backlink-cli/output.ts @@ -1,84 +1,24 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional pretty rendering + * @output agent-first JSON output (built on @deniffer/cli-kit) * @pos serialization boundary between backlink handlers and terminal */ -import { inspect } from "node:util"; -import type { CliContext } from "./context"; -import { normalizeCliError } from "./lib/errors"; - -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; - -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } +import type { CliContext } from "./context"; +import { backlinkErrorMapper } from "./lib/errors"; - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: CliContext): OutputService { - const pretty = context.pretty ?? false; - - return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, - error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - - if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); - return; - } - - const lines = human ?? [ - `Error Code: ${resolved.code}`, - `Error: ${resolved.message}`, - ...(resolved.hint ? [`Hint: ${resolved.hint}`] : []), - ]; - process.stderr.write(`${lines.join("\n")}\n`); - }, - }; + return createBaseOutputService({ + pretty: context.pretty, + errorMappers: [backlinkErrorMapper], + }); } diff --git a/packages/backlink-cli/package.json b/packages/backlink-cli/package.json index 9d974b1..10fd413 100644 --- a/packages/backlink-cli/package.json +++ b/packages/backlink-cli/package.json @@ -37,9 +37,10 @@ "test:once": "vitest run" }, "dependencies": { + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0" }, diff --git a/packages/backlink-cli/services.ts b/packages/backlink-cli/services.ts index f350853..cc28a86 100644 --- a/packages/backlink-cli/services.ts +++ b/packages/backlink-cli/services.ts @@ -1,31 +1,30 @@ /** * @input runtime context, provider factory, and output service - * @output lazy service container for backlink handlers + * @output lazy service container for backlink handlers (built on @deniffer/cli-kit) * @pos runtime composition layer for provider and serializer boundaries */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import type { CliContext } from "./context"; -import { createOutputService, type OutputService } from "./output"; -import { createBacklinkClient, type BacklinkClient } from "./provider"; +import { backlinkErrorMapper } from "./lib/errors"; +import { type BacklinkClient, createBacklinkClient } from "./provider"; + +const backlinkAdapter = defineClientAdapter((context) => + createBacklinkClient({ login: context.login, password: context.password }), +); -export type CliServices = { - context: CliContext; - output: OutputService; +export type CliServices = BaseCliServices & { getBacklinkClient: () => BacklinkClient; }; export function createCliServices(context: CliContext): CliServices { - let backlinkClient: BacklinkClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getBacklinkClient() { - backlinkClient ??= createBacklinkClient({ - login: context.login, - password: context.password, - }); - return backlinkClient; - }, - }; + adapter: backlinkAdapter, + errorMappers: [backlinkErrorMapper], + }); + + return { ...base, getBacklinkClient: base.getClient }; } diff --git a/packages/backlink-cli/vitest.config.ts b/packages/backlink-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/backlink-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/google-ads-cli/lib/errors.ts b/packages/google-ads-cli/lib/errors.ts index b29d5b0..f919782 100644 --- a/packages/google-ads-cli/lib/errors.ts +++ b/packages/google-ads-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** - * @input machine-classified CLI error metadata - * @output stable CliError shape for agent-facing runtime failures - * @pos shared error contract between handlers, client, and output + * @input machine-classified Google Ads provider failures + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos Google Ads error contract = cli-kit core + a Google Ads-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "not_found" @@ -13,26 +22,6 @@ export type CliErrorCode = | "provider_rate_limited" | "provider_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - type ProviderError = { message?: string; error_code?: Record; @@ -49,7 +38,12 @@ function hasProviderError(errors: ProviderError[], key: string) { return errors.some((item) => Boolean(item.error_code?.[key])); } -function normalizeProviderError(error: Error) { +// The Google Ads-specific divergence, plugged into cli-kit's normalizer seam. +export const googleAdsErrorMapper: CliErrorMapper = (error) => { + if (!(error instanceof Error)) { + return null; + } + const status = Number(Reflect.get(error, "status") ?? 0); const providerErrors = readProviderErrors(error); @@ -106,23 +100,10 @@ function normalizeProviderError(error: Error) { }); } + // Defer Error-without-status and non-Error to cli-kit's backend_failure fallback. return null; -} +}; export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - - if (error instanceof Error) { - return ( - normalizeProviderError(error) ?? - cliError({ code: "backend_failure", message: error.message }) - ); - } - - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); + return normalizeWithMappers(error, [googleAdsErrorMapper]); } diff --git a/packages/google-ads-cli/output.ts b/packages/google-ads-cli/output.ts index 47ca8cd..92bc7a2 100644 --- a/packages/google-ads-cli/output.ts +++ b/packages/google-ads-cli/output.ts @@ -1,77 +1,38 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional human-friendly rendering - * @pos CLI serialization boundary between handlers and terminal + * @output agent-first JSON output (built on @deniffer/cli-kit) + * @pos serialization boundary between Google Ads handlers and terminal */ -import { inspect } from "node:util"; -import { normalizeCliError } from "./lib/errors"; +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; +import { googleAdsErrorMapper, normalizeCliError } from "./lib/errors"; -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} - -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } - - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: { pretty?: boolean }): OutputService { const pretty = context.pretty ?? false; + const base = createBaseOutputService({ + pretty, + errorMappers: [googleAdsErrorMapper], + }); return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, + success: base.success, error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - + // JSON mode + normalization are delegated to cli-kit; only the pretty + // error rendering preserves this CLI's pinned "Error Code:" layout. if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); + base.error(error, human); return; } + const resolved = normalizeCliError(error); const lines = human ?? [ `Error Code: ${resolved.code}`, `Error: ${resolved.message}`, diff --git a/packages/google-ads-cli/package.json b/packages/google-ads-cli/package.json index 4dc5a48..6c6d2f2 100644 --- a/packages/google-ads-cli/package.json +++ b/packages/google-ads-cli/package.json @@ -43,7 +43,8 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0" }, diff --git a/packages/google-ads-cli/services.ts b/packages/google-ads-cli/services.ts index 4467c9f..e2211dc 100644 --- a/packages/google-ads-cli/services.ts +++ b/packages/google-ads-cli/services.ts @@ -1,31 +1,29 @@ /** - * @input resolved CLI context, Google Ads client factory, and output service - * @output lazy CLI service container for handlers - * @pos Google Ads runtime services boundary + * @input runtime context, Google Ads client factory, and output service + * @output lazy service container for Google Ads handlers (built on @deniffer/cli-kit) + * @pos runtime composition layer for provider and serializer boundaries */ -import { - type CliContext, - createGoogleAdsClient, - type GoogleAdsClient, -} from "./client"; -import { createOutputService, type OutputService } from "./output"; +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; -export type CliServices = { - context: CliContext; - output: OutputService; +import { type CliContext, createGoogleAdsClient, type GoogleAdsClient } from "./client"; +import { googleAdsErrorMapper } from "./lib/errors"; + +const googleAdsAdapter = defineClientAdapter((context) => + createGoogleAdsClient(context), +); + +export type CliServices = BaseCliServices & { getGoogleAdsClient: () => GoogleAdsClient; }; export function createCliServices(context: CliContext): CliServices { - let googleAdsClient: GoogleAdsClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getGoogleAdsClient() { - googleAdsClient ??= createGoogleAdsClient(context); - return googleAdsClient; - }, - }; + adapter: googleAdsAdapter, + errorMappers: [googleAdsErrorMapper], + }); + + return { ...base, getGoogleAdsClient: base.getClient }; } diff --git a/packages/google-ads-cli/vitest.config.ts b/packages/google-ads-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/google-ads-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/gsc-cli/lib/errors.ts b/packages/gsc-cli/lib/errors.ts index 28af8a2..542e5e1 100644 --- a/packages/gsc-cli/lib/errors.ts +++ b/packages/gsc-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified CLI error metadata - * @output stable CliError shape for agent-facing runtime failures - * @pos shared error contract between handlers, client, and output + * @output stable CliError shape for agent-facing runtime failures (built on @deniffer/cli-kit) + * @pos GSC error contract = cli-kit core + a Google-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "not_found" @@ -13,26 +22,6 @@ export type CliErrorCode = | "provider_rate_limited" | "provider_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - function readProviderReason(error: Error) { const response = Reflect.get(error, "response") as | { data?: { error?: { errors?: Array<{ reason?: string }> } } } @@ -45,13 +34,19 @@ function readProviderReason(error: Error) { ); } -function normalizeGoogleProviderError(error: Error) { +// The GSC-specific divergence, plugged into cli-kit's normalizer seam. +export const gscErrorMapper: CliErrorMapper = (error) => { + if (!(error instanceof Error)) { + return null; + } + const status = Number( Reflect.get(error, "status") ?? Reflect.get(error, "code") ?? 0 ); const reason = readProviderReason(error); if (!status) { + // Defer Error-without-status to cli-kit's backend_failure fallback. return null; } @@ -105,22 +100,8 @@ function normalizeGoogleProviderError(error: Error) { ? "Retry later; Google returned a server error." : undefined, }); -} +}; export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - - if (error instanceof Error) { - return ( - normalizeGoogleProviderError(error) ?? - cliError({ code: "backend_failure", message: error.message }) - ); - } - - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); + return normalizeWithMappers(error, [gscErrorMapper]); } diff --git a/packages/gsc-cli/output.ts b/packages/gsc-cli/output.ts index 47ca8cd..0210e6a 100644 --- a/packages/gsc-cli/output.ts +++ b/packages/gsc-cli/output.ts @@ -1,77 +1,37 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional human-friendly rendering - * @pos CLI serialization boundary between handlers and terminal + * @output agent-first JSON output with optional human-friendly rendering (built on @deniffer/cli-kit) + * @pos serialization boundary between GSC handlers and terminal */ -import { inspect } from "node:util"; -import { normalizeCliError } from "./lib/errors"; +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; +import { normalizeCliError } from "@deniffer/cli-kit/errors"; -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; +import { gscErrorMapper } from "./lib/errors"; -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} - -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } - - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: { pretty?: boolean }): OutputService { const pretty = context.pretty ?? false; + const base = createBaseOutputService({ pretty, errorMappers: [gscErrorMapper] }); return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, + success: base.success, error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - + // Non-pretty (agent JSON) goes through cli-kit verbatim. if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); + base.error(error, human); return; } + // GSC pins a "Error Code: …\nError: …" pretty contract that predates + // cli-kit's single-line format; preserve it while reusing the shared mapper. + const resolved = normalizeCliError(error, [gscErrorMapper]); const lines = human ?? [ `Error Code: ${resolved.code}`, `Error: ${resolved.message}`, diff --git a/packages/gsc-cli/package.json b/packages/gsc-cli/package.json index ad66ccd..08750ce 100644 --- a/packages/gsc-cli/package.json +++ b/packages/gsc-cli/package.json @@ -39,7 +39,8 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "googleapis": "^166.0.0", "valibot": "^1.2.0" diff --git a/packages/gsc-cli/services.ts b/packages/gsc-cli/services.ts index 253fbbc..aaa52a6 100644 --- a/packages/gsc-cli/services.ts +++ b/packages/gsc-cli/services.ts @@ -1,27 +1,29 @@ /** * @input resolved CLI context, GSC client factory, and output service - * @output lazy CLI service container for handlers + * @output lazy service container for GSC handlers (built on @deniffer/cli-kit) * @pos GSC runtime services boundary */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import { type CliContext, createGscClient, type GscClient } from "./client"; -import { createOutputService, type OutputService } from "./output"; +import { gscErrorMapper } from "./lib/errors"; + +const gscAdapter = defineClientAdapter((context) => + createGscClient(context), +); -export type CliServices = { - context: CliContext; - output: OutputService; +export type CliServices = BaseCliServices & { getGscClient: () => GscClient; }; export function createCliServices(context: CliContext): CliServices { - let gscClient: GscClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getGscClient() { - gscClient ??= createGscClient(context); - return gscClient; - }, - }; + adapter: gscAdapter, + errorMappers: [gscErrorMapper], + }); + + return { ...base, getGscClient: base.getClient }; } diff --git a/packages/gsc-cli/vitest.config.ts b/packages/gsc-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/gsc-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/page-extract-cli/lib/errors.ts b/packages/page-extract-cli/lib/errors.ts index 8cf1ccb..0588b98 100644 --- a/packages/page-extract-cli/lib/errors.ts +++ b/packages/page-extract-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified page-extract runtime failures - * @output stable agent-facing error objects - * @pos shared error contract for extraction provider and output + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos page-extract error contract = cli-kit core + a page-extract-specific mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "not_found" @@ -11,31 +20,8 @@ export type CliErrorCode = | "parse_error" | "backend_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - -export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - +// The page-extract-specific divergence, plugged into cli-kit's normalizer seam. +export const pageExtractErrorMapper: CliErrorMapper = (error) => { if (error instanceof TypeError) { return cliError({ code: "network_error", @@ -44,15 +30,10 @@ export function normalizeCliError(error: unknown) { }); } - if (error instanceof Error) { - return cliError({ - code: "backend_failure", - message: error.message, - }); - } + // Defer Error-without-special-case and non-Error to cli-kit's backend_failure fallback. + return null; +}; - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); +export function normalizeCliError(error: unknown) { + return normalizeWithMappers(error, [pageExtractErrorMapper]); } diff --git a/packages/page-extract-cli/output.ts b/packages/page-extract-cli/output.ts index eafa446..80a7a28 100644 --- a/packages/page-extract-cli/output.ts +++ b/packages/page-extract-cli/output.ts @@ -1,84 +1,24 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional pretty rendering + * @output agent-first JSON output (built on @deniffer/cli-kit) * @pos serialization boundary between page-extract handlers and terminal */ -import { inspect } from "node:util"; -import type { CliContext } from "./context"; -import { normalizeCliError } from "./lib/errors"; - -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; - -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } +import type { CliContext } from "./context"; +import { pageExtractErrorMapper } from "./lib/errors"; - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: CliContext): OutputService { - const pretty = context.pretty ?? false; - - return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, - error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - - if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); - return; - } - - const lines = human ?? [ - `Error Code: ${resolved.code}`, - `Error: ${resolved.message}`, - ...(resolved.hint ? [`Hint: ${resolved.hint}`] : []), - ]; - process.stderr.write(`${lines.join("\n")}\n`); - }, - }; + return createBaseOutputService({ + pretty: context.pretty, + errorMappers: [pageExtractErrorMapper], + }); } diff --git a/packages/page-extract-cli/package.json b/packages/page-extract-cli/package.json index df7e2ea..ed2fcda 100644 --- a/packages/page-extract-cli/package.json +++ b/packages/page-extract-cli/package.json @@ -39,7 +39,8 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "valibot": "^1.2.0" }, "devDependencies": { diff --git a/packages/page-extract-cli/services.ts b/packages/page-extract-cli/services.ts index f0989d7..f9ccc97 100644 --- a/packages/page-extract-cli/services.ts +++ b/packages/page-extract-cli/services.ts @@ -1,28 +1,30 @@ /** * @input runtime context, provider factory, and output service - * @output lazy service container for page-extract handlers + * @output lazy service container for page-extract handlers (built on @deniffer/cli-kit) * @pos runtime composition layer for provider and serializer boundaries */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import type { CliContext } from "./context"; -import { createOutputService, type OutputService } from "./output"; -import { createPageExtractClient, type PageExtractClient } from "./provider"; +import { pageExtractErrorMapper } from "./lib/errors"; +import { type PageExtractClient, createPageExtractClient } from "./provider"; + +const pageExtractAdapter = defineClientAdapter((context) => + createPageExtractClient({ ctxBin: context.ctxBin }), +); -export type CliServices = { - context: CliContext; - output: OutputService; +export type CliServices = BaseCliServices & { getPageExtractClient: () => PageExtractClient; }; export function createCliServices(context: CliContext): CliServices { - let pageExtractClient: PageExtractClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getPageExtractClient() { - pageExtractClient ??= createPageExtractClient({ ctxBin: context.ctxBin }); - return pageExtractClient; - }, - }; + adapter: pageExtractAdapter, + errorMappers: [pageExtractErrorMapper], + }); + + return { ...base, getPageExtractClient: base.getClient }; } diff --git a/packages/page-extract-cli/vitest.config.ts b/packages/page-extract-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/page-extract-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/posthog-cli/lib/errors.ts b/packages/posthog-cli/lib/errors.ts index 452b96a..53d17a5 100644 --- a/packages/posthog-cli/lib/errors.ts +++ b/packages/posthog-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified PostHog provider failures - * @output stable agent-facing error objects - * @pos shared error contract for PostHog handlers and provider calls + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos PostHog error contract = cli-kit core + a PostHog-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "auth_error" @@ -13,82 +22,55 @@ export type CliErrorCode = | "parse_error" | "backend_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - function readStatus(error: Error) { const status = Reflect.get(error, "status"); return typeof status === "number" ? status : null; } -export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - +// The PostHog-specific divergence, plugged into cli-kit's normalizer seam. +export const posthogErrorMapper: CliErrorMapper = (error) => { if (error instanceof TypeError) { return cliError({ code: "network_error", message: error.message, - hint: "Check network reachability and retry the PostHog request.", + hint: "Check network reachability and retry the PostHog request.", }); } - if (error instanceof Error) { - const status = readStatus(error); - - if (status === 401 || status === 403) { - return cliError({ - code: "auth_error", - message: error.message, - hint: "Check the PostHog personal API key, host, project ID, and scopes.", - }); - } - - if (status === 402 || status === 429) { - return cliError({ - code: "quota_error", - message: error.message, - hint: "Check PostHog plan access, rate limits, and retry policy.", - }); - } - - if (status !== null) { - return cliError({ - code: "provider_error", - message: error.message, - hint: - status >= 500 - ? "PostHog returned a server error. Retry later." - : "PostHog rejected the request. Check input fields.", - }); - } + if (!(error instanceof Error)) { + return null; + } + const status = readStatus(error); + if (status === 401 || status === 403) { + return cliError({ + code: "auth_error", + message: error.message, + hint: "Check the PostHog personal API key, host, project ID, and scopes.", + }); + } + if (status === 402 || status === 429) { return cliError({ - code: "backend_failure", + code: "quota_error", message: error.message, + hint: "Check PostHog plan access, rate limits, and retry policy.", }); } + if (status !== null) { + return cliError({ + code: "provider_error", + message: error.message, + hint: + status >= 500 + ? "PostHog returned a server error. Retry later." + : "PostHog rejected the request. Check input fields.", + }); + } + + // Defer Error-without-status and non-Error to cli-kit's backend_failure fallback. + return null; +}; - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); +export function normalizeCliError(error: unknown) { + return normalizeWithMappers(error, [posthogErrorMapper]); } diff --git a/packages/posthog-cli/output.ts b/packages/posthog-cli/output.ts index 41383b1..9f70c4a 100644 --- a/packages/posthog-cli/output.ts +++ b/packages/posthog-cli/output.ts @@ -1,84 +1,24 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional pretty rendering + * @output agent-first JSON output (built on @deniffer/cli-kit) * @pos serialization boundary between PostHog handlers and terminal */ -import { inspect } from "node:util"; -import type { CliContext } from "./context"; -import { normalizeCliError } from "./lib/errors"; - -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; - -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } +import type { CliContext } from "./context"; +import { posthogErrorMapper } from "./lib/errors"; - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: CliContext): OutputService { - const pretty = context.pretty ?? false; - - return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, - error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - - if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); - return; - } - - const lines = human ?? [ - `Error Code: ${resolved.code}`, - `Error: ${resolved.message}`, - ...(resolved.hint ? [`Hint: ${resolved.hint}`] : []), - ]; - process.stderr.write(`${lines.join("\n")}\n`); - }, - }; + return createBaseOutputService({ + pretty: context.pretty, + errorMappers: [posthogErrorMapper], + }); } diff --git a/packages/posthog-cli/package.json b/packages/posthog-cli/package.json index 2d78d48..1d96ca0 100644 --- a/packages/posthog-cli/package.json +++ b/packages/posthog-cli/package.json @@ -42,7 +42,8 @@ "@posthog/agent-toolkit": "^0.2.4", "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0" }, diff --git a/packages/posthog-cli/services.ts b/packages/posthog-cli/services.ts index 6ae986c..a58519c 100644 --- a/packages/posthog-cli/services.ts +++ b/packages/posthog-cli/services.ts @@ -1,32 +1,34 @@ /** * @input runtime context, official PostHog toolkit adapter, and output service - * @output lazy service container for posthog CLI handlers + * @output lazy service container for posthog CLI handlers (built on @deniffer/cli-kit) * @pos runtime composition layer for PostHog provider reads and serializer boundaries */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import type { CliContext } from "./context"; -import { createOutputService, type OutputService } from "./output"; +import { posthogErrorMapper } from "./lib/errors"; import { createPostHogClient, type PostHogClient } from "./provider"; -export type CliServices = { - context: CliContext; - output: OutputService; +const posthogAdapter = defineClientAdapter((context) => + createPostHogClient({ + apiToken: context.apiToken, + apiBaseUrl: context.apiBaseUrl, + projectId: context.projectId, + }), +); + +export type CliServices = BaseCliServices & { getPostHogClient: () => PostHogClient; }; export function createCliServices(context: CliContext): CliServices { - let posthogClient: PostHogClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getPostHogClient() { - posthogClient ??= createPostHogClient({ - apiToken: context.apiToken, - apiBaseUrl: context.apiBaseUrl, - projectId: context.projectId, - }); - return posthogClient; - }, - }; + adapter: posthogAdapter, + errorMappers: [posthogErrorMapper], + }); + + return { ...base, getPostHogClient: base.getClient }; } diff --git a/packages/posthog-cli/vitest.config.ts b/packages/posthog-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/posthog-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/serp-snapshot-cli/lib/errors.ts b/packages/serp-snapshot-cli/lib/errors.ts index 281cdbd..07b097f 100644 --- a/packages/serp-snapshot-cli/lib/errors.ts +++ b/packages/serp-snapshot-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified SERP provider failures - * @output stable agent-facing error objects - * @pos shared error contract for SERP snapshot handlers and provider calls + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos SERP error contract = cli-kit core + a SERP-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "auth_error" @@ -13,36 +22,13 @@ export type CliErrorCode = | "parse_error" | "backend_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - function readStatus(error: Error) { const status = Reflect.get(error, "status"); return typeof status === "number" ? status : null; } -export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - +// The SERP-specific divergence, plugged into cli-kit's normalizer seam. +export const serpSnapshotErrorMapper: CliErrorMapper = (error) => { if (error instanceof TypeError) { return cliError({ code: "network_error", @@ -51,44 +37,40 @@ export function normalizeCliError(error: unknown) { }); } - if (error instanceof Error) { - const status = readStatus(error); - - if (status === 401 || status === 403) { - return cliError({ - code: "auth_error", - message: error.message, - hint: "Check SERP provider credentials.", - }); - } - - if (status === 402 || status === 429) { - return cliError({ - code: "quota_error", - message: error.message, - hint: "Check SERP provider credits, rate limits, and retry policy.", - }); - } - - if (status !== null) { - return cliError({ - code: "provider_error", - message: error.message, - hint: - status >= 500 - ? "The SERP provider returned a server error. Retry later." - : "The SERP provider rejected the request. Check input fields.", - }); - } + if (!(error instanceof Error)) { + return null; + } + const status = readStatus(error); + if (status === 401 || status === 403) { + return cliError({ + code: "auth_error", + message: error.message, + hint: "Check SERP provider credentials.", + }); + } + if (status === 402 || status === 429) { return cliError({ - code: "backend_failure", + code: "quota_error", message: error.message, + hint: "Check SERP provider credits, rate limits, and retry policy.", }); } + if (status !== null) { + return cliError({ + code: "provider_error", + message: error.message, + hint: + status >= 500 + ? "The SERP provider returned a server error. Retry later." + : "The SERP provider rejected the request. Check input fields.", + }); + } + + // Defer Error-without-status and non-Error to cli-kit's backend_failure fallback. + return null; +}; - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); +export function normalizeCliError(error: unknown) { + return normalizeWithMappers(error, [serpSnapshotErrorMapper]); } diff --git a/packages/serp-snapshot-cli/output.ts b/packages/serp-snapshot-cli/output.ts index 9f3345f..ba93e66 100644 --- a/packages/serp-snapshot-cli/output.ts +++ b/packages/serp-snapshot-cli/output.ts @@ -1,84 +1,24 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional pretty rendering + * @output agent-first JSON output (built on @deniffer/cli-kit) * @pos serialization boundary between SERP handlers and terminal */ -import { inspect } from "node:util"; -import type { CliContext } from "./context"; -import { normalizeCliError } from "./lib/errors"; - -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; - -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; - -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, - }); -} +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } +import type { CliContext } from "./context"; +import { serpSnapshotErrorMapper } from "./lib/errors"; - return typeof human === "function" ? human(data) : human; -} +export type { HumanLines, Output, OutputService }; export function createOutputService(context: CliContext): OutputService { - const pretty = context.pretty ?? false; - - return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, - error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - - if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); - return; - } - - const lines = human ?? [ - `Error Code: ${resolved.code}`, - `Error: ${resolved.message}`, - ...(resolved.hint ? [`Hint: ${resolved.hint}`] : []), - ]; - process.stderr.write(`${lines.join("\n")}\n`); - }, - }; + return createBaseOutputService({ + pretty: context.pretty, + errorMappers: [serpSnapshotErrorMapper], + }); } diff --git a/packages/serp-snapshot-cli/package.json b/packages/serp-snapshot-cli/package.json index 5cbfd4c..21a32b4 100644 --- a/packages/serp-snapshot-cli/package.json +++ b/packages/serp-snapshot-cli/package.json @@ -39,7 +39,8 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "dotenv": "^17.2.2", "valibot": "^1.2.0" }, diff --git a/packages/serp-snapshot-cli/services.ts b/packages/serp-snapshot-cli/services.ts index 77bbc24..91a61b7 100644 --- a/packages/serp-snapshot-cli/services.ts +++ b/packages/serp-snapshot-cli/services.ts @@ -1,34 +1,30 @@ /** * @input runtime context, provider factory, and output service - * @output lazy service container for serp-snapshot handlers + * @output lazy service container for serp-snapshot handlers (built on @deniffer/cli-kit) * @pos runtime composition layer for provider and serializer boundaries */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import type { CliContext } from "./context"; -import { createOutputService, type OutputService } from "./output"; -import { - createSerpSnapshotClient, - type SerpSnapshotClient, -} from "./provider"; +import { serpSnapshotErrorMapper } from "./lib/errors"; +import { createSerpSnapshotClient, type SerpSnapshotClient } from "./provider"; + +const serpSnapshotAdapter = defineClientAdapter((context) => + createSerpSnapshotClient({ login: context.login, password: context.password }), +); -export type CliServices = { - context: CliContext; - output: OutputService; +export type CliServices = BaseCliServices & { getSerpSnapshotClient: () => SerpSnapshotClient; }; export function createCliServices(context: CliContext): CliServices { - let serpSnapshotClient: SerpSnapshotClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getSerpSnapshotClient() { - serpSnapshotClient ??= createSerpSnapshotClient({ - login: context.login, - password: context.password, - }); - return serpSnapshotClient; - }, - }; + adapter: serpSnapshotAdapter, + errorMappers: [serpSnapshotErrorMapper], + }); + + return { ...base, getSerpSnapshotClient: base.getClient }; } diff --git a/packages/serp-snapshot-cli/vitest.config.ts b/packages/serp-snapshot-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/serp-snapshot-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +}); diff --git a/packages/sitemap-watch-cli/lib/errors.ts b/packages/sitemap-watch-cli/lib/errors.ts index 1082f06..876d5c9 100644 --- a/packages/sitemap-watch-cli/lib/errors.ts +++ b/packages/sitemap-watch-cli/lib/errors.ts @@ -1,9 +1,18 @@ /** * @input machine-classified sitemap-watch runtime failures - * @output stable agent-facing error objects - * @pos shared error contract for registry parsing, fetching, and output + * @output stable agent-facing error objects (built on @deniffer/cli-kit) + * @pos sitemap error contract = cli-kit core + a sitemap-specific status mapper */ +import { + CliError, + type CliErrorMapper, + cliError, + normalizeCliError as normalizeWithMappers, +} from "@deniffer/cli-kit/errors"; + +export { CliError, cliError }; + export type CliErrorCode = | "invalid_input" | "not_found" @@ -11,36 +20,13 @@ export type CliErrorCode = | "parse_error" | "backend_failure"; -export class CliError extends Error { - code: CliErrorCode; - hint?: string; - - constructor(input: { code: CliErrorCode; message: string; hint?: string }) { - super(input.message); - this.name = "CliError"; - this.code = input.code; - this.hint = input.hint; - } -} - -export function cliError(input: { - code: CliErrorCode; - message: string; - hint?: string; -}) { - return new CliError(input); -} - function readStatus(error: Error) { const status = Reflect.get(error, "status"); return typeof status === "number" ? status : null; } -export function normalizeCliError(error: unknown) { - if (error instanceof CliError) { - return error; - } - +// The sitemap-specific divergence, plugged into cli-kit's normalizer seam. +export const sitemapErrorMapper: CliErrorMapper = (error) => { if (error instanceof TypeError) { return cliError({ code: "network_error", @@ -49,28 +35,26 @@ export function normalizeCliError(error: unknown) { }); } - if (error instanceof Error) { - const status = readStatus(error); - - if (status !== null) { - return cliError({ - code: "network_error", - message: error.message, - hint: - status >= 500 - ? "The upstream sitemap host returned a server error. Retry later." - : "The sitemap endpoint could not be reached successfully. Check the URL and retry.", - }); - } + if (!(error instanceof Error)) { + return null; + } + const status = readStatus(error); + if (status !== null) { return cliError({ - code: "backend_failure", + code: "network_error", message: error.message, + hint: + status >= 500 + ? "The upstream sitemap host returned a server error. Retry later." + : "The sitemap endpoint could not be reached successfully. Check the URL and retry.", }); } - return cliError({ - code: "backend_failure", - message: "Unknown CLI error", - }); + // Defer Error-without-status and non-Error to cli-kit's backend_failure fallback. + return null; +}; + +export function normalizeCliError(error: unknown) { + return normalizeWithMappers(error, [sitemapErrorMapper]); } diff --git a/packages/sitemap-watch-cli/output.ts b/packages/sitemap-watch-cli/output.ts index d8d9f33..9f9fff1 100644 --- a/packages/sitemap-watch-cli/output.ts +++ b/packages/sitemap-watch-cli/output.ts @@ -1,84 +1,44 @@ /** * @input CLI output mode plus success or error payloads - * @output agent-first JSON output with optional pretty rendering + * @output agent-first JSON output (built on @deniffer/cli-kit) * @pos serialization boundary between sitemap-watch handlers and terminal */ -import { inspect } from "node:util"; -import type { CliContext } from "./context"; -import { normalizeCliError } from "./lib/errors"; - -export type Output = - | { ok: true; data: T } - | { ok: false; error: { code: string; message: string; hint?: string } }; +import { + createOutputService as createBaseOutputService, + type HumanLines, + type Output, + type OutputService, +} from "@deniffer/cli-kit/output"; -export type HumanLines = string[] | ((data: T) => string[]); - -export type OutputService = { - success: (data: T, human?: HumanLines) => void; - error: (error: unknown, human?: string[]) => void; -}; +import type { CliContext } from "./context"; +import { normalizeCliError, sitemapErrorMapper } from "./lib/errors"; -function printJson(value: Output) { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} +export type { HumanLines, Output, OutputService }; -function formatUnknown(value: unknown) { - return inspect(value, { - depth: null, - colors: false, - compact: false, - sorted: true, +export function createOutputService(context: CliContext): OutputService { + const base = createBaseOutputService({ + pretty: context.pretty, + errorMappers: [sitemapErrorMapper], }); -} - -function resolveHumanLines(data: T, human?: HumanLines) { - if (!human) { - return [formatUnknown(data)]; - } - return typeof human === "function" ? human(data) : human; -} - -export function createOutputService(context: CliContext): OutputService { const pretty = context.pretty ?? false; return { - success(data: T, human?: HumanLines) { - if (!pretty) { - printJson({ ok: true, data }); - return; - } - - process.stdout.write(`${resolveHumanLines(data, human).join("\n")}\n`); - }, + success: base.success, error(error: unknown, human?: string[]) { - const resolved = normalizeCliError(error); - - if (!pretty) { - process.stderr.write( - `${JSON.stringify( - { - ok: false, - error: { - code: resolved.code, - message: resolved.message, - ...(resolved.hint ? { hint: resolved.hint } : {}), - }, - } satisfies Output, - null, - 2 - )}\n` - ); + // Preserve sitemap-watch's pretty error contract ("Error Code:" / "Error:") + // while keeping JSON mode, normalization, and the hint line on cli-kit. + if (pretty && !human) { + const resolved = normalizeCliError(error); + base.error(error, [ + `Error Code: ${resolved.code}`, + `Error: ${resolved.message}`, + ]); return; } - const lines = human ?? [ - `Error Code: ${resolved.code}`, - `Error: ${resolved.message}`, - ...(resolved.hint ? [`Hint: ${resolved.hint}`] : []), - ]; - process.stderr.write(`${lines.join("\n")}\n`); + base.error(error, human); }, }; } diff --git a/packages/sitemap-watch-cli/package.json b/packages/sitemap-watch-cli/package.json index 6a69459..e326c7e 100644 --- a/packages/sitemap-watch-cli/package.json +++ b/packages/sitemap-watch-cli/package.json @@ -41,7 +41,8 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", - "argc": "github:ethan-huo/argc", + "@deniffer/cli-kit": "github:Deniffer001/cli-kit#v0.1.2", + "argc": "github:ethan-huo/argc#7d7b60d", "fast-xml-parser": "^5.3.0", "valibot": "^1.2.0" }, diff --git a/packages/sitemap-watch-cli/services.ts b/packages/sitemap-watch-cli/services.ts index 7f7af4e..fd452b7 100644 --- a/packages/sitemap-watch-cli/services.ts +++ b/packages/sitemap-watch-cli/services.ts @@ -1,28 +1,30 @@ /** * @input runtime context, provider factory, and output service - * @output lazy service container for sitemap-watch handlers + * @output lazy service container for sitemap-watch handlers (built on @deniffer/cli-kit) * @pos runtime composition layer for provider and serializer boundaries */ +import { defineClientAdapter } from "@deniffer/cli-kit/client"; +import { type CliServices as BaseCliServices, createCliServices as createBaseCliServices } from "@deniffer/cli-kit/services"; + import type { CliContext } from "./context"; -import { createOutputService, type OutputService } from "./output"; -import { createSitemapClient, type SitemapClient } from "./provider"; +import { sitemapErrorMapper } from "./lib/errors"; +import { type SitemapClient, createSitemapClient } from "./provider"; + +const sitemapAdapter = defineClientAdapter(() => + createSitemapClient(), +); -export type CliServices = { - context: CliContext; - output: OutputService; +export type CliServices = BaseCliServices & { getSitemapClient: () => SitemapClient; }; export function createCliServices(context: CliContext): CliServices { - let sitemapClient: SitemapClient | null = null; - - return { + const base = createBaseCliServices({ context, - output: createOutputService(context), - getSitemapClient() { - sitemapClient ??= createSitemapClient(); - return sitemapClient; - }, - }; + adapter: sitemapAdapter, + errorMappers: [sitemapErrorMapper], + }); + + return { ...base, getSitemapClient: base.getClient }; } diff --git a/packages/sitemap-watch-cli/vitest.config.ts b/packages/sitemap-watch-cli/vitest.config.ts new file mode 100644 index 0000000..7ce6fc1 --- /dev/null +++ b/packages/sitemap-watch-cli/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + server: { + deps: { + // @deniffer/cli-kit ships raw .ts (bun-native); inline it so vitest transpiles it. + inline: [/@deniffer\/cli-kit/], + }, + }, + }, +});