Răsfoiți Sursa

Merge branch 'dev/7.5.x' into support/migrate-to-turbopack

Yuki Takei 1 lună în urmă
părinte
comite
9eb2f55492

+ 22 - 0
.claude/hooks/session-start.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+set -euo pipefail
+
+# Only run in remote (Claude Code on the web) environments
+if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
+  exit 0
+fi
+
+cd "$CLAUDE_PROJECT_DIR"
+
+# Install all workspace dependencies.
+# turbo (root devDependency) and all workspace packages will be installed.
+pnpm install
+
+# Install turbo globally (mirrors devcontainer postCreateCommand.sh) so it is
+# available as a bare `turbo` command in subsequent Claude tool calls.
+# Falls back to adding node_modules/.bin to PATH if the pnpm global store is
+# not yet configured in this environment.
+if ! command -v turbo &> /dev/null; then
+  pnpm install turbo --global 2>/dev/null \
+    || echo "export PATH=\"$CLAUDE_PROJECT_DIR/node_modules/.bin:\$PATH\"" >> "$CLAUDE_ENV_FILE"
+fi

+ 10 - 0
.claude/settings.json

@@ -1,5 +1,15 @@
 {
   "hooks": {
+    "SessionStart": [
+      {
+        "hooks": [
+          {
+            "type": "command",
+            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
+          }
+        ]
+      }
+    ],
     "PostToolUse": [
       {
         "matcher": "Write|Edit",

+ 0 - 11
apps/app/.claude/skills/app-commands/SKILL.md

@@ -39,17 +39,6 @@ pnpm run lint:styles      # Stylelint only
 
 > **Running individual test files**: See the `testing` rule (`.claude/rules/testing.md`).
 
-### Common Mistake
-
-```bash
-# ❌ WRONG: lint:typecheck is NOT a Turborepo task
-turbo run lint:typecheck --filter @growi/app
-# Error: could not find task `lint:typecheck` in project
-
-# ✅ CORRECT: Use pnpm for package-specific scripts
-pnpm --filter @growi/app run lint:typecheck
-```
-
 ## Quick Reference
 
 | Task | Command |

+ 0 - 1
apps/app/package.json

@@ -109,7 +109,6 @@
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
-    "check-node-version": "^4.2.1",
     "compression": "^1.7.4",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^4.6.0",

+ 2 - 2
apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -303,7 +303,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.EDIT)}
               >
                 <span className="badge rounded-pill bg-warning text-dark">
-                  <span className="imaterial-symbols-outlined">edit</span> EDIT
+                  <span className="material-symbols-outlined">edit</span> EDIT
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -314,7 +314,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 checked={triggerEvents.has(TriggerEventType.MOVE)}
                 onChange={() => onChangeTriggerEvents(TriggerEventType.MOVE)}
               >
-                <span className="badge rounded-pill bg-pink">
+                <span className="badge rounded-pill bg-secondary">
                   <span className="material-symbols-outlined">redo</span>MOVE
                 </span>
               </TriggerEventCheckBox>

+ 9 - 0
apps/app/src/features/openai/server/routes/delete-thread.ts

@@ -14,6 +14,7 @@ import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-respo
 import loggerFactory from '~/utils/logger';
 
 import type { IApiv3DeleteThreadParams } from '../../interfaces/thread-relation';
+import ThreadRelationModel from '../models/thread-relation';
 import { getOpenaiService } from '../services/openai';
 import { certifyAiService } from './middlewares/certify-ai-service';
 
@@ -68,6 +69,14 @@ export const deleteThreadFactory = (crowi: Crowi): RequestHandler[] => {
       }
 
       try {
+        const threadRelation = await ThreadRelationModel.findOne({
+          _id: threadRelationId,
+          userId: user._id,
+        });
+        if (threadRelation == null) {
+          return res.apiv3Err(new ErrorV3('Thread not found'), 404);
+        }
+
         const deletedThreadRelation =
           await openaiService.deleteThread(threadRelationId);
         return res.apiv3({ deletedThreadRelation });

+ 1 - 0
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -246,6 +246,7 @@ export const postMessageToEditHandlersFactory = (
 
       const threadRelation = await ThreadRelationModel.findOne({
         threadId: { $eq: threadId },
+        userId: user._id,
       });
       if (threadRelation == null) {
         return res.apiv3Err(new ErrorV3('ThreadRelation not found'), 404);

+ 4 - 2
apps/app/src/features/openai/server/routes/get-threads.ts

@@ -70,8 +70,10 @@ export const getThreadsFactory = (crowi: Crowi): RequestHandler[] => {
           );
         }
 
-        const threads =
-          await openaiService.getThreadsByAiAssistantId(aiAssistantId);
+        const threads = await openaiService.getThreadsByAiAssistantId(
+          aiAssistantId,
+          user._id,
+        );
 
         return res.apiv3({ threads });
       } catch (err) {

+ 9 - 0
apps/app/src/features/openai/server/routes/message/get-messages.ts

@@ -12,6 +12,7 @@ import loginRequiredFactory from '~/server/middlewares/login-required';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
+import ThreadRelationModel from '../../models/thread-relation';
 import { getOpenaiService } from '../../services/openai';
 import { certifyAiService } from '../middlewares/certify-ai-service';
 
@@ -81,6 +82,14 @@ export const getMessagesFactory = (crowi: Crowi): RequestHandler[] => {
           );
         }
 
+        const threadRelation = await ThreadRelationModel.findOne({
+          threadId: { $eq: threadId },
+          userId: user._id,
+        });
+        if (threadRelation == null) {
+          return res.apiv3Err(new ErrorV3('Thread not found'), 404);
+        }
+
         const messages = await openaiService.getMessageData(
           threadId,
           user.lang,

+ 4 - 1
apps/app/src/features/openai/server/routes/message/post-message.ts

@@ -128,7 +128,10 @@ export const postMessageHandlersFactory = (crowi: Crowi): RequestHandler[] => {
         return res.apiv3Err(new ErrorV3('AI assistant not found'), 404);
       }
 
-      const threadRelation = await ThreadRelationModel.findOne({ threadId });
+      const threadRelation = await ThreadRelationModel.findOne({
+        threadId: { $eq: threadId },
+        userId: user._id,
+      });
       if (threadRelation == null) {
         return res.apiv3Err(new ErrorV3('ThreadRelation not found'), 404);
       }

+ 12 - 2
apps/app/src/features/openai/server/services/openai.ts

@@ -99,6 +99,7 @@ export interface IOpenaiService {
   ): Promise<ThreadRelationDocument>;
   getThreadsByAiAssistantId(
     aiAssistantId: string,
+    userId?: string,
   ): Promise<ThreadRelationDocument[]>;
   deleteThread(threadRelationId: string): Promise<ThreadRelationDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
@@ -291,12 +292,21 @@ class OpenaiService implements IOpenaiService {
 
   async getThreadsByAiAssistantId(
     aiAssistantId: string,
+    userId?: string,
     type: ThreadType = ThreadType.KNOWLEDGE,
   ): Promise<ThreadRelationDocument[]> {
-    const threadRelations = await ThreadRelationModel.find({
+    const query: { aiAssistant: string; type: ThreadType; userId?: string } = {
       aiAssistant: aiAssistantId,
       type,
-    }).sort({ updatedAt: -1 });
+    };
+
+    if (userId != null) {
+      query.userId = userId;
+    }
+
+    const threadRelations = await ThreadRelationModel.find(query).sort({
+      updatedAt: -1,
+    });
     return threadRelations;
   }
 

+ 13 - 1
apps/app/src/features/search/client/components/SearchForm.tsx

@@ -41,6 +41,17 @@ export const SearchForm = (props: Props): JSX.Element => {
     [searchKeyword, onSubmit],
   );
 
+  // Prevent Downshift from intercepting Home/End keys so they move
+  // the cursor within the input field instead of navigating the list
+  const keyDownHandler = useCallback(
+    (e: React.KeyboardEvent<HTMLInputElement>) => {
+      if (e.key === 'Home' || e.key === 'End') {
+        e.nativeEvent.preventDownshiftDefault = true;
+      }
+    },
+    [],
+  );
+
   const inputOptions = useMemo(() => {
     return getInputProps({
       type: 'text',
@@ -49,8 +60,9 @@ export const SearchForm = (props: Props): JSX.Element => {
       ref: inputRef,
       value: searchKeyword,
       onChange: changeSearchTextHandler,
+      onKeyDown: keyDownHandler,
     });
-  }, [getInputProps, searchKeyword, changeSearchTextHandler]);
+  }, [getInputProps, searchKeyword, changeSearchTextHandler, keyDownHandler]);
 
   useEffect(() => {
     if (inputRef.current != null) {

+ 9 - 0
apps/app/src/features/search/client/interfaces/downshift.ts

@@ -1,5 +1,14 @@
 import type { ControllerStateAndHelpers } from 'downshift';
 
+// Augment the global Event interface with downshift's custom property.
+// downshift checks event.nativeEvent.preventDownshiftDefault to skip
+// its default key handling. See: https://www.downshift-js.com/downshift#customizing-handlers
+declare global {
+  interface Event {
+    preventDownshiftDefault?: boolean;
+  }
+}
+
 export type DownshiftItem = { url: string };
 
 export type GetItemProps =

+ 150 - 0
apps/app/src/features/search/utils/disable-user-pages.spec.ts

@@ -0,0 +1,150 @@
+import type { QueryTerms } from '~/server/interfaces/search';
+
+import { excludeUserPagesFromQuery } from './disable-user-pages';
+
+describe('excludeUserPagesFromQuery()', () => {
+  it('should exclude user page strings from query prefix', () => {
+    const userString = '/user';
+    const userStringSlash = '/user/';
+    const userStringSubPath = '/user/settings';
+    const userStringSubPathDeep = '/user/profile/edit';
+    const userStringSubPathQuery = '/user/settings?ref=top';
+
+    const query: QueryTerms = {
+      match: [],
+      not_match: [],
+      phrase: [],
+      not_phrase: [],
+      prefix: [
+        userString,
+        userStringSlash,
+        userStringSubPath,
+        userStringSubPathDeep,
+        userStringSubPathQuery,
+      ],
+      not_prefix: [],
+      tag: [],
+      not_tag: [],
+    };
+
+    excludeUserPagesFromQuery(query);
+
+    expect(query.prefix).not.toContain(userString);
+    // Should only contain '/user'
+    expect(query.not_prefix).toContain(userString);
+
+    expect(query.prefix).not.toContain(userStringSlash);
+    expect(query.not_prefix).not.toContain(userStringSlash);
+
+    expect(query.prefix).not.toContain(userStringSubPath);
+    expect(query.not_prefix).not.toContain(userStringSubPath);
+
+    expect(query.prefix).not.toContain(userStringSubPathDeep);
+    expect(query.not_prefix).not.toContain(userStringSubPathDeep);
+
+    expect(query.prefix).not.toContain(userStringSubPathQuery);
+    expect(query.not_prefix).not.toContain(userStringSubPathQuery);
+  });
+
+  it('should not exclude strings similar to /user from query prefix', () => {
+    const userRouter = '/userouter';
+    const userRoot = '/useroot';
+    const userSettings = '/user-settings';
+    const apiUser = '/api/user';
+    const emptyString = '';
+    const rootOnly = '/';
+    const upperCase = '/USER';
+    const doubleSlashStart = '//user/new';
+    const doubleSlashSub = '/user//new';
+
+    const query: QueryTerms = {
+      match: [],
+      not_match: [],
+      phrase: [],
+      not_phrase: [],
+      prefix: [
+        userRouter,
+        userRoot,
+        userSettings,
+        apiUser,
+        emptyString,
+        rootOnly,
+        upperCase,
+        doubleSlashStart,
+        doubleSlashSub,
+      ],
+      not_prefix: [],
+      tag: [],
+      not_tag: [],
+    };
+
+    excludeUserPagesFromQuery(query);
+
+    expect(query.prefix).toContain(userRouter);
+    expect(query.not_prefix).not.toContain(userRouter);
+
+    expect(query.prefix).toContain(userRoot);
+    expect(query.not_prefix).not.toContain(userRoot);
+
+    expect(query.prefix).toContain(userSettings);
+    expect(query.not_prefix).not.toContain(userSettings);
+
+    expect(query.prefix).toContain(apiUser);
+    expect(query.not_prefix).not.toContain(apiUser);
+
+    expect(query.prefix).toContain(emptyString);
+    expect(query.not_prefix).not.toContain(emptyString);
+
+    expect(query.prefix).toContain(rootOnly);
+    expect(query.not_prefix).not.toContain(rootOnly);
+
+    expect(query.prefix).toContain(upperCase);
+    expect(query.not_prefix).not.toContain(upperCase);
+
+    expect(query.prefix).toContain(doubleSlashStart);
+    expect(query.not_prefix).not.toContain(doubleSlashStart);
+
+    expect(query.prefix).toContain(doubleSlashSub);
+    expect(query.not_prefix).not.toContain(doubleSlashSub);
+  });
+
+  it('should add /user to not_prefix when it is empty', () => {
+    const query: QueryTerms = {
+      match: [],
+      not_match: [],
+      phrase: [],
+      not_phrase: [],
+      prefix: [],
+      not_prefix: [],
+      tag: [],
+      not_tag: [],
+    };
+
+    excludeUserPagesFromQuery(query);
+
+    expect(query.prefix).toHaveLength(0);
+    expect(query.not_prefix).toContain('/user');
+    expect(query.not_prefix).toHaveLength(1);
+  });
+
+  it('should remove existing /user strings and leave not_prefix with just one /user string', () => {
+    const userString = '/user';
+
+    const query: QueryTerms = {
+      match: [],
+      not_match: [],
+      phrase: [],
+      not_phrase: [],
+      prefix: [userString, userString],
+      not_prefix: [userString, userString],
+      tag: [],
+      not_tag: [],
+    };
+
+    excludeUserPagesFromQuery(query);
+
+    expect(query.prefix).toHaveLength(0);
+    expect(query.not_prefix).toContain('/user');
+    expect(query.not_prefix).toHaveLength(1);
+  });
+});

+ 10 - 0
apps/app/src/features/search/utils/disable-user-pages.ts

@@ -0,0 +1,10 @@
+import type { QueryTerms } from '~/server/interfaces/search';
+
+export function excludeUserPagesFromQuery(terms: QueryTerms): void {
+  const userRegex: RegExp = /^\/user($|\/(?!\/))/;
+
+  terms.prefix = terms.prefix.filter((p) => !userRegex.test(p));
+  terms.not_prefix = terms.not_prefix.filter((p) => !userRegex.test(p));
+
+  terms.not_prefix.push('/user');
+}

+ 1 - 1
apps/app/src/server/routes/apiv3/admin-home.ts

@@ -96,7 +96,7 @@ module.exports = (crowi: Crowi) => {
       const { getRuntimeVersions } = await import(
         '~/server/util/runtime-versions'
       );
-      const runtimeVersions = await getRuntimeVersions();
+      const runtimeVersions = getRuntimeVersions();
 
       const adminHomeParams: IResAdminHome = {
         growiVersion: getGrowiVersion(),

+ 150 - 0
apps/app/src/server/service/search-query.spec.ts

@@ -0,0 +1,150 @@
+import { vi } from 'vitest';
+import { type MockProxy, mock } from 'vitest-mock-extended';
+
+import { SearchDelegatorName } from '~/interfaces/named-query';
+import type Crowi from '~/server/crowi';
+import { configManager } from '~/server/service/config-manager/config-manager';
+
+import type { SearchDelegator } from '../interfaces/search';
+import NamedQuery from '../models/named-query';
+import SearchService from './search';
+import type ElasticsearchDelegator from './search-delegator/elasticsearch';
+
+// Mock NamedQuery
+vi.mock('~/server/models/named-query', () => {
+  const mockModel = {
+    findOne: vi.fn(),
+  };
+  return {
+    NamedQuery: mockModel,
+    default: mockModel,
+  };
+});
+
+// Mock config manager
+vi.mock('~/server/service/config-manager/config-manager', () => {
+  return {
+    default: {
+      getConfig: vi.fn(),
+    },
+    configManager: {
+      getConfig: vi.fn(),
+    },
+  };
+});
+
+class TestSearchService extends SearchService {
+  override generateFullTextSearchDelegator(): ElasticsearchDelegator {
+    return mock<ElasticsearchDelegator>();
+  }
+
+  override generateNQDelegators(): {
+    [key in SearchDelegatorName]: SearchDelegator;
+  } {
+    return {
+      [SearchDelegatorName.DEFAULT]: mock<SearchDelegator>(),
+      [SearchDelegatorName.PRIVATE_LEGACY_PAGES]: mock<SearchDelegator>(),
+    };
+  }
+
+  override registerUpdateEvent(): void {}
+
+  override get isConfigured(): boolean {
+    return false;
+  }
+}
+
+describe('searchParseQuery()', () => {
+  let searchService: TestSearchService;
+  let mockCrowi: MockProxy<Crowi>;
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+
+    mockCrowi = mock<Crowi>();
+    mockCrowi.configManager = configManager;
+    searchService = new TestSearchService(mockCrowi);
+  });
+
+  it('should contain /user in the not_prefix query when user pages are disabled', async () => {
+    vi.mocked(configManager.getConfig).mockImplementation((key: string) => {
+      if (key === 'security:disableUserPages') {
+        return true;
+      }
+
+      return false;
+    });
+
+    const result = await searchService.parseSearchQuery('/user/settings', null);
+
+    expect(configManager.getConfig).toHaveBeenCalledWith(
+      'security:disableUserPages',
+    );
+    expect(result.terms.not_prefix).toContain('/user');
+    expect(result.terms.prefix).toHaveLength(0);
+  });
+
+  it('should contain /user in the not_prefix even when search query is not a user page', async () => {
+    vi.mocked(configManager.getConfig).mockImplementation((key: string) => {
+      if (key === 'security:disableUserPages') {
+        return true;
+      }
+
+      return false;
+    });
+
+    const result = await searchService.parseSearchQuery('/new-task', null);
+
+    expect(configManager.getConfig).toHaveBeenCalledWith(
+      'security:disableUserPages',
+    );
+    expect(result.terms.not_prefix).toContain('/user');
+    expect(result.terms.prefix).toHaveLength(0);
+  });
+
+  it('should add specific user prefixes in the query when user pages are enabled', async () => {
+    vi.mocked(configManager.getConfig).mockImplementation((key: string) => {
+      if (key === 'security:disableUserPages') {
+        return false;
+      }
+
+      return true;
+    });
+
+    const result = await searchService.parseSearchQuery('/user/settings', null);
+
+    expect(configManager.getConfig).toHaveBeenCalledWith(
+      'security:disableUserPages',
+    );
+    expect(result.terms.not_prefix).not.toContain('/user');
+    expect(result.terms.not_prefix).not.toContain('/user/settings');
+    expect(result.terms.match).toContain('/user/settings');
+  });
+
+  it('should filter user pages even when resolved from a named query alias', async () => {
+    vi.mocked(configManager.getConfig).mockImplementation((key: string) => {
+      if (key === 'security:disableUserPages') {
+        return true;
+      }
+
+      return false;
+    });
+
+    const shortcutName = 'my-shortcut';
+    const aliasPath = '/user/my-private-page';
+
+    // Mock the DB response
+    vi.mocked(NamedQuery.findOne).mockResolvedValue({
+      name: shortcutName,
+      aliasOf: aliasPath,
+    });
+
+    const result = await searchService.parseSearchQuery('dummy', shortcutName);
+
+    expect(configManager.getConfig).toHaveBeenCalledWith(
+      'security:disableUserPages',
+    );
+    expect(result.terms.not_prefix).toContain('/user');
+    expect(result.terms.match).toContain('/user/my-private-page');
+  });
+});

+ 25 - 21
apps/app/src/server/service/search.ts

@@ -8,6 +8,7 @@ import {
   isIncludeAiMenthion,
   removeAiMenthion,
 } from '~/features/search/utils/ai';
+import { excludeUserPagesFromQuery } from '~/features/search/utils/disable-user-pages';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import type {
   IFormattedSearchResult,
@@ -328,34 +329,37 @@ class SearchService implements SearchQueryParser, SearchResolver {
     _queryString: string,
     nqName: string | null,
   ): Promise<ParsedQuery> {
+    const disableUserPages = configManager.getConfig(
+      'security:disableUserPages',
+    );
     const queryString = normalizeQueryString(_queryString);
-
     const terms = this.parseQueryString(queryString);
 
-    if (nqName == null) {
-      return { queryString, terms };
-    }
+    let parsedQuery: ParsedQuery = { queryString, terms };
 
-    const nq = await NamedQuery.findOne({ name: normalizeNQName(nqName) });
+    if (nqName != null) {
+      const nq = await NamedQuery.findOne({ name: normalizeNQName(nqName) });
 
-    // will delegate to full-text search
-    if (nq == null) {
-      logger.debug(
-        `Delegated to full-text search since a named query document did not found. (nqName="${nqName}")`,
-      );
-      return { queryString, terms };
-    }
+      if (nq != null) {
+        const { aliasOf, delegatorName } = nq;
 
-    const { aliasOf, delegatorName } = nq;
+        if (aliasOf != null) {
+          parsedQuery = {
+            queryString: normalizeQueryString(aliasOf),
+            terms: this.parseQueryString(aliasOf),
+          };
+        } else {
+          parsedQuery = { queryString, terms, delegatorName };
+        }
+      } else {
+        logger.debug(
+          `Delegated to full-text search since a named query document did not found. (nqName="${nqName}")`,
+        );
+      }
+    }
 
-    let parsedQuery: ParsedQuery;
-    if (aliasOf != null) {
-      parsedQuery = {
-        queryString: normalizeQueryString(aliasOf),
-        terms: this.parseQueryString(aliasOf),
-      };
-    } else {
-      parsedQuery = { queryString, terms, delegatorName };
+    if (disableUserPages) {
+      excludeUserPagesFromQuery(parsedQuery.terms);
     }
 
     return parsedQuery;

+ 12 - 50
apps/app/src/server/util/runtime-versions.ts

@@ -1,4 +1,4 @@
-import checkNodeVersion from 'check-node-version';
+import { execSync } from 'node:child_process';
 
 type RuntimeVersions = {
   node: string | undefined;
@@ -6,56 +6,18 @@ type RuntimeVersions = {
   pnpm: string | undefined;
 };
 
-// define original types because the object returned is not according to the official type definition
-type SatisfiedVersionInfo = {
-  isSatisfied: true;
-  version: {
-    version: string;
-  };
-};
-
-type NotfoundVersionInfo = {
-  isSatisfied: true;
-  notfound: true;
-};
-
-type VersionInfo = SatisfiedVersionInfo | NotfoundVersionInfo;
-
-function isNotfoundVersionInfo(info: VersionInfo): info is NotfoundVersionInfo {
-  return 'notfound' in info;
-}
-
-function isSatisfiedVersionInfo(
-  info: VersionInfo,
-): info is SatisfiedVersionInfo {
-  return 'version' in info;
-}
-
-const getVersion = (versionInfo: VersionInfo): string | undefined => {
-  if (isNotfoundVersionInfo(versionInfo)) {
+function getCommandVersion(command: string): string | undefined {
+  try {
+    return execSync(command, { encoding: 'utf8' }).trim();
+  } catch {
     return undefined;
   }
+}
 
-  if (isSatisfiedVersionInfo(versionInfo)) {
-    return versionInfo.version.version;
-  }
-
-  return undefined;
-};
-
-export function getRuntimeVersions(): Promise<RuntimeVersions> {
-  return new Promise((resolve, reject) => {
-    checkNodeVersion({}, (error, result) => {
-      if (error) {
-        reject(error);
-        return;
-      }
-
-      resolve({
-        node: getVersion(result.versions.node as unknown as VersionInfo),
-        npm: getVersion(result.versions.npm as unknown as VersionInfo),
-        pnpm: getVersion(result.versions.pnpm as unknown as VersionInfo),
-      });
-    });
-  });
+export function getRuntimeVersions(): RuntimeVersions {
+  return {
+    node: process.versions.node,
+    npm: getCommandVersion('npm --version'),
+    pnpm: getCommandVersion('pnpm --version'),
+  };
 }

+ 1 - 27
pnpm-lock.yaml

@@ -310,9 +310,6 @@ importers:
       bunyan:
         specifier: ^1.8.15
         version: 1.8.15
-      check-node-version:
-        specifier: ^4.2.1
-        version: 4.2.1
       compression:
         specifier: ^1.7.4
         version: 1.7.4
@@ -6685,11 +6682,6 @@ packages:
     resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
     engines: {node: '>= 16'}
 
-  check-node-version@4.2.1:
-    resolution: {integrity: sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==}
-    engines: {node: '>=8.3.0'}
-    hasBin: true
-
   cheerio-select@2.1.0:
     resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
 
@@ -9806,6 +9798,7 @@ packages:
     resolution: {integrity: sha512-Quz3MvAwHxVYNXsOByL7xI5EB2WYOeFswqaHIA3qOK3isRWTxiplBEocmmru6XmxDB2L7jDNYtYA4FyimoAFEw==}
     engines: {node: '>=8.17.0'}
     hasBin: true
+    bundledDependencies: []
 
   jsonfile@3.0.1:
     resolution: {integrity: sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==}
@@ -10324,9 +10317,6 @@ packages:
     resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
     engines: {node: '>=8'}
 
-  map-values@1.0.1:
-    resolution: {integrity: sha512-BbShUnr5OartXJe1GeccAWtfro11hhgNJg6G9/UtWKjVGvV5U4C09cg5nk8JUevhXODaXY+hQ3xxMUKSs62ONQ==}
-
   markdown-it-front-matter@0.2.4:
     resolution: {integrity: sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==}
 
@@ -11209,9 +11199,6 @@ packages:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
 
-  object-filter@1.0.2:
-    resolution: {integrity: sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==}
-
   object-hash@2.2.0:
     resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
     engines: {node: '>= 6'}
@@ -21667,15 +21654,6 @@ snapshots:
 
   check-error@2.1.1: {}
 
-  check-node-version@4.2.1:
-    dependencies:
-      chalk: 3.0.0
-      map-values: 1.0.1
-      minimist: 1.2.8
-      object-filter: 1.0.2
-      run-parallel: 1.2.0
-      semver: 6.3.1
-
   cheerio-select@2.1.0:
     dependencies:
       boolbase: 1.0.0
@@ -25376,8 +25354,6 @@ snapshots:
 
   map-obj@4.3.0: {}
 
-  map-values@1.0.1: {}
-
   markdown-it-front-matter@0.2.4: {}
 
   markdown-it@13.0.2:
@@ -26624,8 +26600,6 @@ snapshots:
 
   object-assign@4.1.1: {}
 
-  object-filter@1.0.2: {}
-
   object-hash@2.2.0: {}
 
   object-inspect@1.13.4: {}