diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8570c4743..a9c5b8442 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -102,6 +102,9 @@ "vip-import-sql-status": "dist/bin/vip-import-sql-status.js", "vip-import-validate-files": "dist/bin/vip-import-validate-files.js", "vip-import-validate-sql": "dist/bin/vip-import-validate-sql.js", + "vip-internal": "dist/bin/vip-internal.js", + "vip-internal-fetch-integrations": "dist/bin/vip-internal-fetch-integrations.js", + "vip-internal-is-logged-in": "dist/bin/vip-internal-is-logged-in.js", "vip-logout": "dist/bin/vip-logout.js", "vip-logs": "dist/bin/vip-logs.js", "vip-search-replace": "dist/bin/vip-search-replace.js", diff --git a/package.json b/package.json index abf57a8b0..024f1d277 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,10 @@ "vip-sync": "dist/bin/vip-sync.js", "vip-whoami": "dist/bin/vip-whoami.js", "vip-wp": "dist/bin/vip-wp.js", - "vip-logout": "dist/bin/vip-logout.js" + "vip-logout": "dist/bin/vip-logout.js", + "vip-internal": "dist/bin/vip-internal.js", + "vip-internal-is-logged-in": "dist/bin/vip-internal-is-logged-in.js", + "vip-internal-fetch-integrations": "dist/bin/vip-internal-fetch-integrations.js" }, "scripts": { "typescript:codegen:install-dependencies": "npm install --no-save @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/near-operation-file-preset @graphql-codegen/typescript-operations", diff --git a/src/bin/vip-internal-fetch-integrations.ts b/src/bin/vip-internal-fetch-integrations.ts new file mode 100755 index 000000000..685fb8a98 --- /dev/null +++ b/src/bin/vip-internal-fetch-integrations.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { disableGlobalGraphQLErrorHandling } from '../lib/api'; +import command from '../lib/cli/command'; +import { fetchIntegrations } from '../lib/dev-environment/integrations'; + +interface Options { + app?: string; + env?: string; +} + +async function fetchIntegrationsCommand( _args: string[], opts: Options ): Promise< void > { + let response: Record< string, unknown >; + if ( opts.app && opts.env ) { + disableGlobalGraphQLErrorHandling(); + try { + response = await fetchIntegrations( opts.app, opts.env ); + } catch ( error: unknown ) { + const err = error instanceof Error ? error : new Error( String( error ) ); + response = { error: err.message }; + process.exitCode = 1; + } + } else { + response = { error: 'Required parameters missing' }; + process.exitCode = 1; + } + + process.stdout.write( JSON.stringify( response ) ); +} + +void command( { usage: 'vip internal fetch-integrations' } ).argv( + process.argv, + fetchIntegrationsCommand +); diff --git a/src/bin/vip-internal-is-logged-in.ts b/src/bin/vip-internal-is-logged-in.ts new file mode 100755 index 000000000..f04a0a1ab --- /dev/null +++ b/src/bin/vip-internal-is-logged-in.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { disableGlobalGraphQLErrorHandling } from '../lib/api'; +import { getCurrentUserInfo } from '../lib/api/user'; +import command from '../lib/cli/command'; + +import type { Me } from '../graphqlTypes'; + +async function isLoggedInCommand(): Promise< void > { + let currentUser: Me; + let response: Record< string, unknown >; + + disableGlobalGraphQLErrorHandling(); + + try { + currentUser = await getCurrentUserInfo( true ); + response = { + displayName: currentUser.displayName, + id: currentUser.id, + isVIP: currentUser.isVIP, + }; + } catch ( err: unknown ) { + const error = err instanceof Error ? err : new Error( 'Unknown error' ); + response = { + error: error.message, + }; + + process.exitCode = 1; + } + + process.stdout.write( JSON.stringify( response ) ); +} + +void command( { usage: 'vip internal is-logged-in' } ).argv( process.argv, isLoggedInCommand ); diff --git a/src/bin/vip-internal.js b/src/bin/vip-internal.js new file mode 100755 index 000000000..a7f499177 --- /dev/null +++ b/src/bin/vip-internal.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import command from '../lib/cli/command'; + +command( { usage: 'vip internal' } ) + .command( 'is-logged-in', 'Check if the user is logged in' ) + .command( 'fetch-integrations', 'Fetch integrations for the given environment' ) + .argv( process.argv ); diff --git a/src/bin/vip.js b/src/bin/vip.js index c4e2cb348..eb5a4eda8 100755 --- a/src/bin/vip.js +++ b/src/bin/vip.js @@ -45,9 +45,18 @@ const runCmd = async function () { .command( 'db', "Access an environment's database." ) .command( 'sync', 'Sync the database from production to a non-production environment.' ) .command( 'whoami', 'Retrieve details about the current authenticated VIP-CLI user.' ) - .command( 'wp', 'Execute a WP-CLI command against an environment.' ); - - cmd.argv( process.argv ); + .command( + 'validate', + 'Scan a Node.js codebase for issues that could prevent building or deploying.' + ) + .command( 'wp', 'Execute a WP-CLI command against an environment.' ) + .command( 'internal', 'Internal commands used by VIP automation tools.' ); + + cmd.argv( process.argv, null, { + usageFilter: usage => + // eslint-disable-next-line no-control-regex + `${ usage }`.replace( /^ {4}(\u001B\[..m)?internal(\u001B\[..m)?(.+)$\n/m, '' ), + } ); }; /** diff --git a/src/lib/cli/command.d.ts b/src/lib/cli/command.d.ts index 974f3c4c2..7445f02a7 100644 --- a/src/lib/cli/command.d.ts +++ b/src/lib/cli/command.d.ts @@ -105,7 +105,11 @@ declare class Args { showHelp(): void; showVersion(): void; - argv: ( argv: string[], cb: unknown ) => Promise< unknown >; + argv: ( + argv: string[], + cb: unknown, + options?: Partial< ConfigurationOptions > + ) => Promise< unknown >; // utils.js handleType( value: unknown ): [ string, ( ( v: unknown ) => unknown )? ]; diff --git a/src/lib/cli/command.js b/src/lib/cli/command.js index ffd3a4047..b1c0216e1 100644 --- a/src/lib/cli/command.js +++ b/src/lib/cli/command.js @@ -40,7 +40,7 @@ let alreadyConfirmedDebugAttachment = false; * @param {string[]} argv */ // eslint-disable-next-line complexity -args.argv = async function ( argv, cb ) { +args.argv = async function ( argv, cb, config = {} ) { if ( process.platform !== 'win32' && argv[ 1 ]?.endsWith( '.js' ) ) { argv[ 1 ] = argv[ 1 ].slice( 0, -3 ); } @@ -55,6 +55,8 @@ args.argv = async function ( argv, cb ) { } const parsedAlias = parseEnvAliasFromArgv( argv ); + this.config = { ...this.config, ...config }; + // A usage option allows us to override the default usage text, which isn't // accurate for subcommands. By default, it will display something like (note // the hyphen): diff --git a/src/lib/dev-environment/dev-environment-core.ts b/src/lib/dev-environment/dev-environment-core.ts index 78f3daf1c..ffa5e4163 100644 --- a/src/lib/dev-environment/dev-environment-core.ts +++ b/src/lib/dev-environment/dev-environment-core.ts @@ -640,8 +640,8 @@ export function getEnvironmentPath( name: string ): string { } export async function getApplicationInformation( - appId: number, - envType: string | null + appId: number | string, + envType: string | null | undefined ): Promise< AppInfo > { // $FlowFixMe: gql template is not supported by flow const fieldsQuery = ` @@ -670,10 +670,10 @@ export async function getApplicationInformation( }, softwareSettings { php { - ...Software + ...Software } wordpress { - ...Software + ...Software } } }`; diff --git a/src/lib/dev-environment/integrations.generated.d.ts b/src/lib/dev-environment/integrations.generated.d.ts new file mode 100644 index 000000000..761b221a4 --- /dev/null +++ b/src/lib/dev-environment/integrations.generated.d.ts @@ -0,0 +1,52 @@ +import * as Types from '../graphqlTypes'; + +export type AppByNameQueryVariables = Types.Exact< { + app?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; + env?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; +} >; + +export type AppByNameQuery = { + __typename?: 'Query'; + apps?: { + __typename?: 'AppList'; + edges?: Array< { + __typename?: 'App'; + id?: number | null; + name?: string | null; + environments?: Array< { + __typename?: 'AppEnvironment'; + id?: number | null; + name?: string | null; + type?: string | null; + getIntegrationsDevEnvConfig?: { + __typename?: 'IntegrationDevEnvConfig'; + data?: any | null; + } | null; + } | null > | null; + } | null > | null; + } | null; +}; + +export type AppByIdQueryVariables = Types.Exact< { + id?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; + env?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; +} >; + +export type AppByIdQuery = { + __typename?: 'Query'; + app?: { + __typename?: 'App'; + id?: number | null; + name?: string | null; + environments?: Array< { + __typename?: 'AppEnvironment'; + id?: number | null; + name?: string | null; + type?: string | null; + getIntegrationsDevEnvConfig?: { + __typename?: 'IntegrationDevEnvConfig'; + data?: any | null; + } | null; + } | null > | null; + } | null; +}; diff --git a/src/lib/dev-environment/integrations.ts b/src/lib/dev-environment/integrations.ts new file mode 100644 index 000000000..69dbe7787 --- /dev/null +++ b/src/lib/dev-environment/integrations.ts @@ -0,0 +1,80 @@ +import gql from 'graphql-tag'; + +import API from '../api'; + +import type { + AppByIdQuery, + AppByIdQueryVariables, + AppByNameQuery, + AppByNameQueryVariables, +} from './integrations.generated'; + +const queryAppByName = gql` + query AppByName($app: String, $env: String) { + apps(first: 1, name: $app) { + edges { + id + name + environments(type: $env) { + id + name + type + getIntegrationsDevEnvConfig { + data + } + } + } + } + } +`; + +const queryAppByID = gql` + query AppByID($id: Int, $env: String) { + app(id: $id) { + id + name + environments(type: $env) { + id + name + type + getIntegrationsDevEnvConfig { + data + } + } + } + } +`; + +export async function fetchIntegrations( + app: string, + env: string +): Promise< Record< string, unknown > > { + type Integrations = Record< string, unknown >; + const api = API( { exitOnError: false, silenceAuthErrors: true } ); + if ( isNaN( Number( app ) ) ) { + const res = await api.query< AppByNameQuery, AppByNameQueryVariables >( { + query: queryAppByName, + variables: { + app, + env, + }, + } ); + + return ( + ( res.data.apps?.edges?.[ 0 ]?.environments?.[ 0 ]?.getIntegrationsDevEnvConfig + ?.data as Integrations ) ?? {} + ); + } + + const res = await api.query< AppByIdQuery, AppByIdQueryVariables >( { + query: queryAppByID, + variables: { + id: Number( app ), + env, + }, + } ); + + return ( + ( res.data.app?.environments?.[ 0 ]?.getIntegrationsDevEnvConfig?.data as Integrations ) ?? {} + ); +}