From 2552b52654c858fe6b039e24df1ea661ba620dca Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 3 Feb 2025 18:21:55 +0200 Subject: [PATCH 1/6] feat(dev-env): add `vip internal` commands to use with codespaces --- package.json | 5 +- ...internal-fetch-integrations.generated.d.ts | 52 +++++++++ src/bin/vip-internal-fetch-integrations.ts | 109 ++++++++++++++++++ src/bin/vip-internal-is-logged-in.ts | 34 ++++++ src/bin/vip-internal.js | 8 ++ src/bin/vip.js | 7 +- .../dev-environment/dev-environment-core.ts | 8 +- 7 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 src/bin/vip-internal-fetch-integrations.generated.d.ts create mode 100755 src/bin/vip-internal-fetch-integrations.ts create mode 100755 src/bin/vip-internal-is-logged-in.ts create mode 100755 src/bin/vip-internal.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.generated.d.ts b/src/bin/vip-internal-fetch-integrations.generated.d.ts new file mode 100644 index 000000000..761b221a4 --- /dev/null +++ b/src/bin/vip-internal-fetch-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/bin/vip-internal-fetch-integrations.ts b/src/bin/vip-internal-fetch-integrations.ts new file mode 100755 index 000000000..16b88c727 --- /dev/null +++ b/src/bin/vip-internal-fetch-integrations.ts @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +import gql from 'graphql-tag'; + +import API, { disableGlobalGraphQLErrorHandling } from '../lib/api'; +import command from '../lib/cli/command'; + +import type { + AppByIdQuery, + AppByIdQueryVariables, + AppByNameQuery, + AppByNameQueryVariables, +} from './vip-internal-fetch-integrations.generated'; + +interface Options { + app?: string; + env?: string; +} + +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 + } + } + } + } +`; + +async function fetchIntegrations( app: string, env: string ): Promise< Record< string, unknown > > { + type Integrations = Record< string, unknown >; + disableGlobalGraphQLErrorHandling(); + 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 ) ?? {} + ); +} + +async function fetchIntegrationsCommand( _args: string[], opts: Options ): Promise< void > { + let response: Record< string, unknown >; + if ( opts.app && opts.env ) { + 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..eb197351f 100755 --- a/src/bin/vip.js +++ b/src/bin/vip.js @@ -45,7 +45,12 @@ 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.' ); + .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 ); }; 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 } } }`; From 67303a750b520755730ec1135ccb08a86ae885ee Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 10 Feb 2025 22:35:05 +0200 Subject: [PATCH 2/6] refactor: extract `fetchIntegrations()` function --- src/bin/vip-internal-fetch-integrations.ts | 81 +------------------ .../integrations.generated.d.ts} | 0 src/lib/dev-environment/integrations.ts | 80 ++++++++++++++++++ 3 files changed, 83 insertions(+), 78 deletions(-) rename src/{bin/vip-internal-fetch-integrations.generated.d.ts => lib/dev-environment/integrations.generated.d.ts} (100%) create mode 100644 src/lib/dev-environment/integrations.ts diff --git a/src/bin/vip-internal-fetch-integrations.ts b/src/bin/vip-internal-fetch-integrations.ts index 16b88c727..685fb8a98 100755 --- a/src/bin/vip-internal-fetch-integrations.ts +++ b/src/bin/vip-internal-fetch-integrations.ts @@ -1,93 +1,18 @@ #!/usr/bin/env node -import gql from 'graphql-tag'; - -import API, { disableGlobalGraphQLErrorHandling } from '../lib/api'; +import { disableGlobalGraphQLErrorHandling } from '../lib/api'; import command from '../lib/cli/command'; - -import type { - AppByIdQuery, - AppByIdQueryVariables, - AppByNameQuery, - AppByNameQueryVariables, -} from './vip-internal-fetch-integrations.generated'; +import { fetchIntegrations } from '../lib/dev-environment/integrations'; interface Options { app?: string; env?: string; } -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 - } - } - } - } -`; - -async function fetchIntegrations( app: string, env: string ): Promise< Record< string, unknown > > { - type Integrations = Record< string, unknown >; - disableGlobalGraphQLErrorHandling(); - 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 ) ?? {} - ); -} - 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 ) { diff --git a/src/bin/vip-internal-fetch-integrations.generated.d.ts b/src/lib/dev-environment/integrations.generated.d.ts similarity index 100% rename from src/bin/vip-internal-fetch-integrations.generated.d.ts rename to src/lib/dev-environment/integrations.generated.d.ts 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 ) ?? {} + ); +} From dcef10862e335b9e8bdccd86b8cdc0ce66d64085 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 10 Feb 2025 22:36:59 +0200 Subject: [PATCH 3/6] chore: regenerate npm-shrinkwrap.json --- npm-shrinkwrap.json | 3 +++ 1 file changed, 3 insertions(+) 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", From 41536d60e23f90f8156b594c91293f59479cfcc3 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Tue, 15 Apr 2025 20:57:48 +0300 Subject: [PATCH 4/6] fix: hide `vip internal` from user --- src/bin/vip.js | 6 +++++- src/lib/cli/command.d.ts | 2 +- src/lib/cli/command.js | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/bin/vip.js b/src/bin/vip.js index eb197351f..8740c17cd 100755 --- a/src/bin/vip.js +++ b/src/bin/vip.js @@ -52,7 +52,11 @@ const runCmd = async function () { .command( 'wp', 'Execute a WP-CLI command against an environment.' ) .command( 'internal', 'Internal commands used by VIP automation tools.' ); - cmd.argv( process.argv ); + cmd.argv( process.argv, null, { + usageFilter: usage => + // eslint-disable-next-line no-control-regex + `${ usage }`.replace( /^ {4}(\u001B....)?internal(\u001B....)?(.+)$\n/m, '' ), + } ); }; /** diff --git a/src/lib/cli/command.d.ts b/src/lib/cli/command.d.ts index 974f3c4c2..cef1159c9 100644 --- a/src/lib/cli/command.d.ts +++ b/src/lib/cli/command.d.ts @@ -105,7 +105,7 @@ declare class Args { showHelp(): void; showVersion(): void; - argv: ( argv: string[], cb: unknown ) => Promise< unknown >; + argv: ( argv: string[], cb: unknown, options?: Partial ) => 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): From 7678ae4e9604ff7c4e4ea6e0e90265c2d5abad3a Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 10 Feb 2025 23:42:58 +0200 Subject: [PATCH 5/6] refactor: make regex more specific --- src/bin/vip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/vip.js b/src/bin/vip.js index 8740c17cd..eb5a4eda8 100755 --- a/src/bin/vip.js +++ b/src/bin/vip.js @@ -55,7 +55,7 @@ const runCmd = async function () { cmd.argv( process.argv, null, { usageFilter: usage => // eslint-disable-next-line no-control-regex - `${ usage }`.replace( /^ {4}(\u001B....)?internal(\u001B....)?(.+)$\n/m, '' ), + `${ usage }`.replace( /^ {4}(\u001B\[..m)?internal(\u001B\[..m)?(.+)$\n/m, '' ), } ); }; From fa7f302107545f488b022e0c59795a4464760cda Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 10 Feb 2025 23:45:47 +0200 Subject: [PATCH 6/6] style: fix --- src/lib/cli/command.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/cli/command.d.ts b/src/lib/cli/command.d.ts index cef1159c9..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, options?: Partial ) => Promise< unknown >; + argv: ( + argv: string[], + cb: unknown, + options?: Partial< ConfigurationOptions > + ) => Promise< unknown >; // utils.js handleType( value: unknown ): [ string, ( ( v: unknown ) => unknown )? ];