diff --git a/.changeset/seven-onions-reply.md b/.changeset/seven-onions-reply.md new file mode 100644 index 00000000..cc31dc8b --- /dev/null +++ b/.changeset/seven-onions-reply.md @@ -0,0 +1,5 @@ +--- +'@watching/cli': patch +--- + +Add `--debug` option for commands that run GraphQL queries diff --git a/packages/cli/source/cli.ts b/packages/cli/source/cli.ts index e4e75471..b8f1ca28 100644 --- a/packages/cli/source/cli.ts +++ b/packages/cli/source/cli.ts @@ -44,12 +44,20 @@ async function run() { switch (command) { case 'sign-in': { const {signIn} = await import('./commands/sign-in'); - await signIn({ui}); + const {'--debug': debug} = arg( + {'--debug': Boolean}, + {argv: remainingArgs, permissive: true}, + ); + await signIn({ui, debug}); break; } case 'sign-out': { const {signOut} = await import('./commands/sign-out'); - await signOut({ui}); + const {'--debug': debug} = arg( + {'--debug': Boolean}, + {argv: remainingArgs, permissive: true}, + ); + await signOut({ui, debug}); break; } case 'create': { @@ -74,12 +82,20 @@ async function run() { } case 'push': { const {push} = await import('./commands/push'); - await push({ui}); + const {'--debug': debug} = arg( + {'--debug': Boolean}, + {argv: remainingArgs, permissive: true}, + ); + await push({ui, debug}); break; } case 'publish': { const {publish} = await import('./commands/publish'); - await publish({ui}); + const {'--debug': debug} = arg( + {'--debug': Boolean}, + {argv: remainingArgs, permissive: true}, + ); + await publish({ui, debug}); break; } default: { diff --git a/packages/cli/source/commands/publish/publish.ts b/packages/cli/source/commands/publish/publish.ts index 072fc231..2d8722ec 100644 --- a/packages/cli/source/commands/publish/publish.ts +++ b/packages/cli/source/commands/publish/publish.ts @@ -16,8 +16,8 @@ import type { import publishLatestClipsExtensionVersion from './graphql/PublishLatestClipsExtensionVersionMutation.graphql'; -export async function publish({ui}: {ui: Ui}) { - const {graphql} = await authenticate({ui}); +export async function publish({ui, debug}: {ui: Ui; debug?: boolean}) { + const {graphql} = await authenticate({ui, debug}); const localApp = await loadLocalApp(); diff --git a/packages/cli/source/commands/push/push.ts b/packages/cli/source/commands/push/push.ts index e6f0677f..f0a291d4 100644 --- a/packages/cli/source/commands/push/push.ts +++ b/packages/cli/source/commands/push/push.ts @@ -33,7 +33,7 @@ type ConfigurationField = NonNullable< ConfigurationFieldInput[keyof ConfigurationFieldInput] >; -export async function push({ui}: {ui: Ui}) { +export async function push({ui, debug = false}: {ui: Ui; debug?: boolean}) { const localApp = await loadLocalApp(); if (localApp.extensions.length === 0) { @@ -49,7 +49,7 @@ export async function push({ui}: {ui: Ui}) { verifyLocalBuild(localApp, ui); - const authenticatedContext = await authenticate({ui}); + const authenticatedContext = await authenticate({ui, debug}); const {graphql} = authenticatedContext; diff --git a/packages/cli/source/commands/sign-in/sign-in.ts b/packages/cli/source/commands/sign-in/sign-in.ts index 9fa1b9cf..61c29dce 100644 --- a/packages/cli/source/commands/sign-in/sign-in.ts +++ b/packages/cli/source/commands/sign-in/sign-in.ts @@ -4,8 +4,8 @@ import { userFromLocalAuthentication, } from '../../utilities/authentication'; -export async function signIn({ui}: {ui: Ui}) { - const existingUser = await userFromLocalAuthentication(); +export async function signIn({ui, debug}: {ui: Ui; debug?: boolean}) { + const existingUser = await userFromLocalAuthentication({ui, debug}); if (existingUser) { ui.Heading('success!', {style: (content, style) => style.green(content)}); @@ -19,7 +19,7 @@ export async function signIn({ui}: {ui: Ui}) { return; } - const user = await authenticate({ui}); + const user = await authenticate({ui, debug}); ui.Heading('success!', {style: (content, style) => style.green(content)}); ui.TextBlock( diff --git a/packages/cli/source/commands/sign-out/sign-out.ts b/packages/cli/source/commands/sign-out/sign-out.ts index 9f0cf961..ebdbc7d6 100644 --- a/packages/cli/source/commands/sign-out/sign-out.ts +++ b/packages/cli/source/commands/sign-out/sign-out.ts @@ -1,8 +1,8 @@ import type {Ui} from '../../ui'; import {deleteAuthentication} from '../../utilities/authentication'; -export async function signOut({ui}: {ui: Ui}) { - await deleteAuthentication(); +export async function signOut({ui, debug}: {ui: Ui; debug?: boolean}) { + await deleteAuthentication({ui, debug}); ui.Heading('success!', {style: (content, style) => style.green(content)}); ui.TextBlock( diff --git a/packages/cli/source/utilities/authentication/authentication.ts b/packages/cli/source/utilities/authentication/authentication.ts index 9b03ef40..1819fc74 100644 --- a/packages/cli/source/utilities/authentication/authentication.ts +++ b/packages/cli/source/utilities/authentication/authentication.ts @@ -5,7 +5,11 @@ import * as path from 'path'; import {writeFile, mkdir, rm as remove, readFile} from 'fs/promises'; import open from 'open'; -import {createGraphQLFetch, type GraphQLFetch} from '@quilted/graphql'; +import { + createGraphQLFetch, + type GraphQLFetch, + type GraphQLFetchContext, +} from '@quilted/graphql'; import {PrintableError} from '../../ui'; import type {Ui} from '../../ui'; @@ -27,10 +31,17 @@ export interface User { const USER_CACHE_DIRECTORY = path.resolve(homedir(), '.watch'); const CREDENTIALS_FILE = path.resolve(USER_CACHE_DIRECTORY, 'credentials'); -export async function authenticate({ui}: {ui: Ui}): Promise { +export async function authenticate({ + ui, + debug, +}: { + ui: Ui; + debug?: boolean; +}): Promise { if (process.env.WATCH_ACCESS_TOKEN) { const userFromEnvironmentAccessToken = await userFromAccessToken( process.env.WATCH_ACCESS_TOKEN, + {ui, debug}, ); if (userFromEnvironmentAccessToken == null) { @@ -47,20 +58,29 @@ export async function authenticate({ui}: {ui: Ui}): Promise { } } - const alreadyAuthenticatedUser = await userFromLocalAuthentication(); + const alreadyAuthenticatedUser = await userFromLocalAuthentication({ + ui, + debug, + }); if (alreadyAuthenticatedUser) return alreadyAuthenticatedUser; - const user = await authenticateFromWebAuthentication({ui}); + const user = await authenticateFromWebAuthentication({ui, debug}); return user; } -export async function deleteAuthentication() { +export async function deleteAuthentication({ + ui, + debug, +}: { + ui: Ui; + debug?: boolean; +}) { const accessToken = await accessTokenFromCacheDirectory(); if (accessToken) { - const mutate = graphqlFromAccessToken(accessToken); + const mutate = graphqlFromAccessToken(accessToken, {ui, debug}); await mutate(deleteAccessTokenForCliMutation, { variables: {token: accessToken}, }); @@ -69,13 +89,21 @@ export async function deleteAuthentication() { } } -export async function userFromLocalAuthentication() { +export async function userFromLocalAuthentication({ + ui, + debug, +}: { + ui: Ui; + debug?: boolean; +}) { const accessTokenFromRoot = await accessTokenFromCacheDirectory(); if (accessTokenFromRoot == null) return; - const userFromRootAccessToken = - await userFromAccessToken(accessTokenFromRoot); + const userFromRootAccessToken = await userFromAccessToken( + accessTokenFromRoot, + {ui, debug}, + ); if (userFromRootAccessToken == null) { await remove(USER_CACHE_DIRECTORY, {recursive: true, force: true}); @@ -97,19 +125,66 @@ async function accessTokenFromCacheDirectory(): Promise { } } -function graphqlFromAccessToken(accessToken: string) { - return createGraphQLFetch({ +function graphqlFromAccessToken( + accessToken: string, + {ui, debug = false}: {ui: Ui; debug?: boolean}, +) { + const baseFetchGraphQL = createGraphQLFetch({ url: watchUrl('/api/graphql'), headers: { 'X-Access-Token': accessToken, }, }); + + if (!debug) return baseFetchGraphQL; + + return async function fetchGraphQL(query, options) { + const context: GraphQLFetchContext = {}; + + ui.TextBlock(`[debug] Performing GraphQL query: ${(query as any).name}`, { + style: (content, style) => style.dim(content), + }); + ui.TextBlock((query as any).source, { + style: (content, style) => style.dim(content), + }); + ui.TextBlock(`Variables: ${JSON.stringify(options?.variables ?? {})}`, { + style: (content, style) => style.dim(content), + }); + + const result = await baseFetchGraphQL(query, options, context); + + if (context.request) { + ui.TextBlock( + `[debug] Performed GraphQL request: ${context.request.method.toUpperCase()} ${ + context.request.url + }`, + { + style: (content, style) => style.dim(content), + }, + ); + } + + ui.TextBlock( + `[debug] GraphQL response: ${(query as any).name} (status: ${ + context.response?.status ?? 'unknown' + })`, + { + style: (content, style) => style.dim(content), + }, + ); + ui.TextBlock(JSON.stringify(result), { + style: (content, style) => style.dim(content), + }); + + return result; + } satisfies GraphQLFetch; } async function userFromAccessToken( accessToken: string, + {ui, debug = false}: {ui: Ui; debug?: boolean}, ): Promise { - const graphql = graphqlFromAccessToken(accessToken); + const graphql = graphqlFromAccessToken(accessToken, {ui, debug}); const {data} = await graphql(checkAuthFromCliQuery); @@ -119,17 +194,22 @@ async function userFromAccessToken( } export async function authenticateFromWebAuthentication({ + ui, to = '/app/developer/cli/authenticate', + debug = false, ...rest }: Omit & { + ui: Ui; to?: PerformWebAuthenticationOptions['to']; + debug?: boolean; }) { const {token} = await performWebAuthentication<{token: string}>({ + ui, to, ...rest, }); - const user = await userFromAccessToken(token); + const user = await userFromAccessToken(token, {ui, debug}); if (user == null) { throw new PrintableError(