diff --git a/package-lock.json b/package-lock.json index bb25a75e..12b6218e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -817,7 +817,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -839,7 +838,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" }, @@ -876,7 +874,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -1416,7 +1413,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -1854,8 +1850,7 @@ "version": "0.31.28", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node10": { "version": "1.0.11", @@ -1922,7 +1917,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.28.tgz", "integrity": "sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2126,7 +2120,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3275,7 +3268,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@fastify/ajv-compiler": "^3.5.0", "@fastify/error": "^3.4.0", @@ -5213,6 +5205,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", @@ -5264,7 +5257,8 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pg/node_modules/postgres-array": { "version": "2.0.0", @@ -5561,7 +5555,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6774,7 +6767,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6978,7 +6970,6 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7052,7 +7043,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7166,7 +7156,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7180,7 +7169,6 @@ "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "3.0.9", "@vitest/mocker": "3.0.9", diff --git a/src/server/constants.ts b/src/server/constants.ts index c64b45e6..cd197765 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -76,3 +76,5 @@ export const DEFAULT_POOL_CONFIG: PoolConfig = { } export const PG_META_REQ_HEADER = process.env.PG_META_REQ_HEADER || 'request-id' + + diff --git a/src/server/server.ts b/src/server/server.ts index 8b7c1c10..e79b9d31 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,213 +1,218 @@ -import closeWithGrace from 'close-with-grace' -import { pino } from 'pino' -import { PostgresMeta } from '../lib/index.js' -import { build as buildApp } from './app.js' -import { build as buildAdminApp } from './admin-app.js' -import { - DEFAULT_POOL_CONFIG, - EXPORT_DOCS, - GENERATE_TYPES, - GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, - GENERATE_TYPES_INCLUDED_SCHEMAS, - GENERATE_TYPES_SWIFT_ACCESS_CONTROL, - PG_CONNECTION, - PG_META_HOST, - PG_META_PORT, - POSTGREST_VERSION, -} from './constants.js' -import { apply as applyTypescriptTemplate } from './templates/typescript.js' -import { apply as applyGoTemplate } from './templates/go.js' -import { apply as applySwiftTemplate } from './templates/swift.js' - -const logger = pino({ - formatters: { - level(label) { - return { level: label } - }, - }, - timestamp: pino.stdTimeFunctions.isoTime, -}) - -const app = buildApp({ logger }) -const adminApp = buildAdminApp({ logger }) - -async function getTypeOutput(): Promise { - const pgMeta: PostgresMeta = new PostgresMeta({ - ...DEFAULT_POOL_CONFIG, - connectionString: PG_CONNECTION, - }) - const [ - { data: schemas, error: schemasError }, - { data: tables, error: tablesError }, - { data: foreignTables, error: foreignTablesError }, - { data: views, error: viewsError }, - { data: materializedViews, error: materializedViewsError }, - { data: columns, error: columnsError }, - { data: relationships, error: relationshipsError }, - { data: functions, error: functionsError }, - { data: types, error: typesError }, - ] = await Promise.all([ - pgMeta.schemas.list(), - pgMeta.tables.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.foreignTables.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.views.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.materializedViews.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.columns.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - }), - pgMeta.relationships.list(), - pgMeta.functions.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - }), - pgMeta.types.list({ - includeTableTypes: true, - includeArrayTypes: true, - includeSystemSchemas: true, - }), - ]) - await pgMeta.end() - - if (schemasError) { - throw new Error(schemasError.message) - } - if (tablesError) { - throw new Error(tablesError.message) - } - if (foreignTablesError) { - throw new Error(foreignTablesError.message) - } - if (viewsError) { - throw new Error(viewsError.message) - } - if (materializedViewsError) { - throw new Error(materializedViewsError.message) - } - if (columnsError) { - throw new Error(columnsError.message) - } - if (relationshipsError) { - throw new Error(relationshipsError.message) - } - if (functionsError) { - throw new Error(functionsError.message) - } - if (typesError) { - throw new Error(typesError.message) - } - - const config = { - schemas: schemas!.filter( - ({ name }) => - GENERATE_TYPES_INCLUDED_SCHEMAS.length === 0 || - GENERATE_TYPES_INCLUDED_SCHEMAS.includes(name) - ), - tables: tables!, - foreignTables: foreignTables!, - views: views!, - materializedViews: materializedViews!, - columns: columns!, - relationships: relationships!, - functions: functions!.filter( - ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) - ), - types: types!, - detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, - postgrestVersion: POSTGREST_VERSION, - } - - switch (GENERATE_TYPES?.toLowerCase()) { - case 'typescript': - return await applyTypescriptTemplate(config) - case 'swift': - return await applySwiftTemplate({ - ...config, - accessControl: GENERATE_TYPES_SWIFT_ACCESS_CONTROL, - }) - case 'go': - return applyGoTemplate(config) - default: - throw new Error(`Unsupported language for GENERATE_TYPES: ${GENERATE_TYPES}`) - } -} - -if (EXPORT_DOCS) { - // TODO: Move to a separate script. - await app.ready() - // @ts-ignore: app.swagger() is a Fastify decorator, so doesn't show up in the types - console.log(JSON.stringify(app.swagger(), null, 2)) -} else if (GENERATE_TYPES) { - console.log(await getTypeOutput()) -} else { - const closeListeners = closeWithGrace(async ({ err, signal, manual }) => { - if (err) { - app.log.error({ err }, 'server closing with error') - } else { - app.log.error( - { err: new Error('Signal Received') }, - `${signal} signal received, server closing, close manual received: ${manual}` - ) - } - try { - await app.close() - } catch (err) { - app.log.error({ err }, `Failed to close app`) - throw err - } - try { - await adminApp.close() - } catch (err) { - app.log.error({ err }, `Failed to close adminApp`) - throw err - } - }) - app.addHook('onClose', async () => { - try { - closeListeners.uninstall() - await adminApp.close() - } catch (err) { - app.log.error({ err }, `Failed to close adminApp in app onClose hook`) - throw err - } - }) - adminApp.addHook('onClose', async () => { - try { - closeListeners.uninstall() - await app.close() - } catch (err) { - app.log.error({ err }, `Failed to close app in adminApp onClose hook`) - throw err - } - }) - - app.listen({ port: PG_META_PORT, host: PG_META_HOST }, (err) => { - if (err) { - app.log.error({ err }, 'Uncaught error in app, exit(1)') - process.exit(1) - } - const adminPort = PG_META_PORT + 1 - adminApp.listen({ port: adminPort, host: PG_META_HOST }, (err) => { - if (err) { - app.log.error({ err }, 'Uncaught error in adminApp, exit(1)') - process.exit(1) - } - }) - }) -} +import closeWithGrace from 'close-with-grace' +import { pino } from 'pino' +import { PostgresMeta } from '../lib/index.js' +import { build as buildApp } from './app.js' +import { build as buildAdminApp } from './admin-app.js' +import { + DEFAULT_POOL_CONFIG, + EXPORT_DOCS, + GENERATE_TYPES, + GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, + GENERATE_TYPES_INCLUDED_SCHEMAS, + GENERATE_TYPES_SWIFT_ACCESS_CONTROL, + PG_CONNECTION, + PG_META_HOST, + PG_META_PORT, + POSTGREST_VERSION, +} from './constants.js' +import { apply as applyTypescriptTemplate } from './templates/typescript.js' +import { apply as applyGoTemplate } from './templates/go.js' +import { apply as applySwiftTemplate } from './templates/swift.js' +import { isRunningInWSL } from './utils.js' +const logger = pino({ + formatters: { + level(label) { + return { level: label } + }, + }, + timestamp: pino.stdTimeFunctions.isoTime, +}) + +const app = buildApp({ logger }) +const adminApp = buildAdminApp({ logger }) + +async function getTypeOutput(): Promise { + const pgMeta: PostgresMeta = new PostgresMeta({ + ...DEFAULT_POOL_CONFIG, + connectionString: PG_CONNECTION, + }) + const [ + { data: schemas, error: schemasError }, + { data: tables, error: tablesError }, + { data: foreignTables, error: foreignTablesError }, + { data: views, error: viewsError }, + { data: materializedViews, error: materializedViewsError }, + { data: columns, error: columnsError }, + { data: relationships, error: relationshipsError }, + { data: functions, error: functionsError }, + { data: types, error: typesError }, + ] = await Promise.all([ + pgMeta.schemas.list(), + pgMeta.tables.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + includeColumns: false, + }), + pgMeta.foreignTables.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + includeColumns: false, + }), + pgMeta.views.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + includeColumns: false, + }), + pgMeta.materializedViews.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + includeColumns: false, + }), + pgMeta.columns.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + }), + pgMeta.relationships.list(), + pgMeta.functions.list({ + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + }), + pgMeta.types.list({ + includeTableTypes: true, + includeArrayTypes: true, + includeSystemSchemas: true, + }), + ]) + await pgMeta.end() + + if (schemasError) { + throw new Error(schemasError.message) + } + if (tablesError) { + throw new Error(tablesError.message) + } + if (foreignTablesError) { + throw new Error(foreignTablesError.message) + } + if (viewsError) { + throw new Error(viewsError.message) + } + if (materializedViewsError) { + throw new Error(materializedViewsError.message) + } + if (columnsError) { + throw new Error(columnsError.message) + } + if (relationshipsError) { + throw new Error(relationshipsError.message) + } + if (functionsError) { + throw new Error(functionsError.message) + } + if (typesError) { + throw new Error(typesError.message) + } + + const config = { + schemas: schemas!.filter( + ({ name }) => + GENERATE_TYPES_INCLUDED_SCHEMAS.length === 0 || + GENERATE_TYPES_INCLUDED_SCHEMAS.includes(name) + ), + tables: tables!, + foreignTables: foreignTables!, + views: views!, + materializedViews: materializedViews!, + columns: columns!, + relationships: relationships!, + functions: functions!.filter( + ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) + ), + types: types!, + detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, + postgrestVersion: POSTGREST_VERSION, + } + + switch (GENERATE_TYPES?.toLowerCase()) { + case 'typescript': + return await applyTypescriptTemplate(config) + case 'swift': + return await applySwiftTemplate({ + ...config, + accessControl: GENERATE_TYPES_SWIFT_ACCESS_CONTROL, + }) + case 'go': + return applyGoTemplate(config) + default: + throw new Error(`Unsupported language for GENERATE_TYPES: ${GENERATE_TYPES}`) + } +} + +if (EXPORT_DOCS) { + // TODO: Move to a separate script. + await app.ready() + // @ts-ignore: app.swagger() is a Fastify decorator, so doesn't show up in the types + console.log(JSON.stringify(app.swagger(), null, 2)) +} else if (GENERATE_TYPES) { + console.log(await getTypeOutput()) +} else { + const closeListeners = closeWithGrace(async ({ err, signal, manual }) => { + if (err) { + app.log.error({ err }, 'server closing with error') + } else { + app.log.error( + { err: new Error('Signal Received') }, + `${signal} signal received, server closing, close manual received: ${manual}` + ) + } + try { + await app.close() + } catch (err) { + app.log.error({ err }, `Failed to close app`) + throw err + } + try { + await adminApp.close() + } catch (err) { + app.log.error({ err }, `Failed to close adminApp`) + throw err + } + }) + app.addHook('onClose', async () => { + try { + closeListeners.uninstall() + await adminApp.close() + } catch (err) { + app.log.error({ err }, `Failed to close adminApp in app onClose hook`) + throw err + } + }) + adminApp.addHook('onClose', async () => { + try { + closeListeners.uninstall() + await app.close() + } catch (err) { + app.log.error({ err }, `Failed to close app in adminApp onClose hook`) + throw err + } + }) + + app.listen({ port: PG_META_PORT, host: PG_META_HOST }, (err,address) => { + if (err) { + app.log.error({ err }, 'Uncaught error in app, exit(1)') + process.exit(1) + }else{ + if (isRunningInWSL()) { + app.log.info(`[WSL Detected] Server is running. Access from your Windows browser at http://localhost:${PG_META_PORT}`) + app.log.info(`(Internal address: ${address})`) + } + } + const adminPort = PG_META_PORT + 1 + adminApp.listen({ port: adminPort, host: PG_META_HOST }, (err) => { + if (err) { + app.log.error({ err }, 'Uncaught error in adminApp, exit(1)') + process.exit(1) + } + }) + }) +} diff --git a/src/server/utils.ts b/src/server/utils.ts index ebb8ec90..1bac7b05 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,50 +1,72 @@ -import pgcs from 'pg-connection-string' -import { FastifyRequest } from 'fastify' -import { DEFAULT_POOL_CONFIG } from './constants.js' -import { PoolConfig } from '../lib/types.js' - -export const extractRequestForLogging = (request: FastifyRequest) => { - let pg: string = 'unknown' - try { - if (request.headers.pg) { - pg = pgcs.parse(request.headers.pg as string).host || pg - } - } catch (e: any) { - console.warn('failed to parse PG connstring for ' + request.url) - } - - const additional = request.headers['x-supabase-info']?.toString() || '' - - return { - method: request.method, - url: request.url, - pg, - opt: additional, - } -} - -export function createConnectionConfig(request: FastifyRequest): PoolConfig { - const connectionString = request.headers.pg as string - const config = { ...DEFAULT_POOL_CONFIG, connectionString } - - // Override application_name if custom one provided in header - if (request.headers['x-pg-application-name']) { - config.application_name = request.headers['x-pg-application-name'] as string - } - - return config -} - -export function translateErrorToResponseCode( - error: { message: string }, - defaultResponseCode = 400 -): number { - if (error.message === 'Connection terminated due to connection timeout') { - return 504 - } else if (error.message === 'sorry, too many clients already') { - return 503 - } else if (error.message === 'Query read timeout') { - return 408 - } - return defaultResponseCode -} +import fs from 'fs' +import os from 'os' +import pgcs from 'pg-connection-string' +import { FastifyRequest } from 'fastify' +import { DEFAULT_POOL_CONFIG } from './constants.js' +import { PoolConfig } from '../lib/types.js' + +export const extractRequestForLogging = (request: FastifyRequest) => { + let pg: string = 'unknown' + try { + if (request.headers.pg) { + pg = pgcs.parse(request.headers.pg as string).host || pg + } + } catch (e: any) { + console.warn('failed to parse PG connstring for ' + request.url) + } + + const additional = request.headers['x-supabase-info']?.toString() || '' + + return { + method: request.method, + url: request.url, + pg, + opt: additional, + } +} + +export function createConnectionConfig(request: FastifyRequest): PoolConfig { + const connectionString = request.headers.pg as string + const config = { ...DEFAULT_POOL_CONFIG, connectionString } + + // Override application_name if custom one provided in header + if (request.headers['x-pg-application-name']) { + config.application_name = request.headers['x-pg-application-name'] as string + } + + return config +} + +export function translateErrorToResponseCode( + error: { message: string }, + defaultResponseCode = 400 +): number { + if (error.message === 'Connection terminated due to connection timeout') { + return 504 + } else if (error.message === 'sorry, too many clients already') { + return 503 + } else if (error.message === 'Query read timeout') { + return 408 + } + return defaultResponseCode +} + +export function isRunningInWSL() { + // Check for the presence of a specific file that only exists in WSL + if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) { + return true + } + + // Check for environment variables (less reliable as a user could set these manually) + if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) { + return true + } + + // Check the OS release info (kernel name often contains 'microsoft') + const osRelease = os.release(); + if (osRelease.includes('microsoft') || osRelease.includes('Microsoft')) { + return true + } + + return false +}