diff --git a/cli/package.json b/cli/package.json index 6f2b8e3c4..97d44eb84 100644 --- a/cli/package.json +++ b/cli/package.json @@ -53,6 +53,8 @@ "miniflare": "^2.11.0", "node-fetch": "^3.3.0", "open": "^8.4.1", + "pino": "^8.10.0", + "pino-pretty": "^9.2.0", "prompts": "^2.4.2", "relaxed-json": "^1.0.3", "rollup-plugin-esbuild": "^5.0.0", diff --git a/cli/src/base.ts b/cli/src/base.ts index 511d0994e..164872faf 100644 --- a/cli/src/base.ts +++ b/cli/src/base.ts @@ -1,4 +1,4 @@ -import { Command, Config, Flags, Interfaces } from '@oclif/core'; +import { Command, Flags, Interfaces } from '@oclif/core'; import { getAPIKey, getCurrentBranchName, @@ -14,8 +14,10 @@ import { cosmiconfigSync } from 'cosmiconfig'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; import { readFile, writeFile } from 'fs/promises'; +import compact from 'lodash.compact'; import fetch from 'node-fetch'; import path from 'path'; +import pino from 'pino'; import prompts from 'prompts'; import table from 'text-table'; import which from 'which'; @@ -30,6 +32,8 @@ import { } from './credentials.js'; import { reportBugURL } from './utils.js'; +const logLevels = ['debug', 'info', 'warn', 'error'] as const; + export const projectConfigSchema = z.object({ databaseURL: z.string(), codegen: z.object({ @@ -82,6 +86,8 @@ export abstract class BaseCommand extends Command { #xataClient?: XataApiClient; + #logger!: pino.Logger; + // The first place is the one used by default when running `xata init` // In the future we can support YAML searchPlaces = [`.${moduleName}rc`, `.${moduleName}rc.json`, 'package.json']; @@ -128,6 +134,16 @@ export abstract class BaseCommand extends Command { helpGroup: commonFlagsHelpGroup, helpValue: '', description: 'Profile name to use' + }), + 'log-level': Flags.custom<(typeof logLevels)[number]>({ + summary: 'Specify level for logging.', + options: Object.values(logLevels), + helpGroup: commonFlagsHelpGroup, + description: `Specify level for logging. Possible values: ${Object.values(logLevels).join(', ')}.` + })(), + 'log-file': Flags.string({ + helpGroup: commonFlagsHelpGroup, + description: 'Specify file to persist all logs.' }) }; @@ -151,6 +167,19 @@ export abstract class BaseCommand extends Command { } async init() { + const { flags } = await this.parseCommand(); + const { ['log-file']: logFile, ['log-level']: logLevel = 'info' } = flags; + + this.#logger = pino({ + level: logLevel, + transport: { + targets: compact([ + { level: logLevel, target: 'pino-pretty', options: { colorize: true } }, + logFile ? { level: 'trace', target: 'pino/file', options: { destination: logFile } } : undefined + ]) + } + }); + if (process.env.XATA_API_KEY) this.apiKeyLocation = 'shell'; for (const envFile of ENV_FILES) { this.loadEnvFile(envFile); @@ -262,14 +291,57 @@ export abstract class BaseCommand extends Command { }); } - info(message: string) { - this.log(`${chalk.blueBright('i')} ${message}`); + log(message?: string) { + this.#logger.info(message); + return super.log(message); } - success(message: string) { - this.log(`${chalk.greenBright('✔')} ${message}`); + info(message?: string) { + this.#logger.info(message); + return super.log(`${chalk.blueBright('i')} ${message}`); } + success(message?: string) { + this.#logger.info(message); + return super.log(`${chalk.greenBright('✔')} ${message}`); + } + + warn(input: string | Error) { + this.#logger.warn(input instanceof Error ? input.message : input); + return super.warn(input); + } + + error( + input: string | Error, + options?: { + /** + * messsage to display related to the error + */ + message?: string; + /** + * a unique error code for this error class + */ + code?: string; + /** + * a url to find out more information related to this error + * or fixing the error + */ + ref?: string; + /** + * a suggestion that may be useful or provide additional context + */ + suggestions?: string[]; + } + ) { + this.#logger.error(input instanceof Error ? input.message : input); + return super.error(input, options); + } + + debug = (...args: any[]) => { + this.#logger.debug(args); + return super.debug(args); + }; + async verifyAPIKey(profile: Profile) { this.info('Checking access to the API...'); const xata = await this.getXataClient(profile); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7a54ec17..ff0a35f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,8 @@ importers: node-fetch: ^3.3.0 oclif: ^3.6.3 open: ^8.4.1 + pino: ^8.10.0 + pino-pretty: ^9.2.0 prompts: ^2.4.2 relaxed-json: ^1.0.3 rollup-plugin-esbuild: ^5.0.0 @@ -191,6 +193,8 @@ importers: miniflare: 2.11.0 node-fetch: 3.3.0 open: 8.4.1 + pino: 8.10.0 + pino-pretty: 9.2.0 prompts: 2.4.2 relaxed-json: 1.0.3 rollup-plugin-esbuild: 5.0.0 @@ -8236,7 +8240,6 @@ packages: /colorette/2.0.19: resolution: { integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== } - dev: true /colors-option/2.0.1: resolution: @@ -8811,7 +8814,6 @@ packages: /dateformat/4.6.3: resolution: { integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== } - dev: true /deasync/0.1.28: resolution: @@ -11111,6 +11113,11 @@ packages: - supports-color dev: false + /fast-copy/3.0.0: + resolution: + { integrity: sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA== } + dev: false + /fast-decode-uri-component/1.0.1: resolution: { integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== } @@ -12471,6 +12478,14 @@ packages: { integrity: sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA== } dev: true + /help-me/4.2.0: + resolution: + { integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== } + dependencies: + glob: 8.0.3 + readable-stream: 3.6.0 + dev: false + /highlight.js/10.7.3: resolution: { integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== } @@ -17458,11 +17473,50 @@ packages: split2: 4.1.0 dev: false + /pino-pretty/9.2.0: + resolution: + { integrity: sha512-7CeszmFqrUD08+JvtYcFXowNE7duFlE1XScmR41qTMbwQOhn7gijYYrRb5udH+z8xq4+A8vN8rQNYVPCTfzmGw== } + hasBin: true + dependencies: + colorette: 2.0.19 + dateformat: 4.6.3 + fast-copy: 3.0.0 + fast-safe-stringify: 2.1.1 + help-me: 4.2.0 + joycon: 3.1.1 + minimist: 1.2.7 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pump: 3.0.0 + readable-stream: 4.3.0 + secure-json-parse: 2.6.0 + sonic-boom: 3.2.1 + strip-json-comments: 3.1.1 + dev: false + /pino-std-serializers/6.1.0: resolution: { integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== } dev: false + /pino/8.10.0: + resolution: + { integrity: sha512-ODfIe+giJtQGsvNAEj5/sHHpL3TFBg161JBH4W62Hc0l0PJjsDFD1R7meLI4PZ2aoHDJznxFNShkJcaG/qJToQ== } + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.1.2 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.1.0 + process-warning: 2.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.1 + sonic-boom: 3.2.1 + thread-stream: 2.2.0 + dev: false + /pino/8.8.0: resolution: { integrity: sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ== } @@ -19899,7 +19953,6 @@ packages: resolution: { integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== } engines: { node: '>=8' } - dev: true /strip-literal/1.0.0: resolution: