feat(editor): Allow users to update verified nodes from the node settings panel (#16447)

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Dana
2025-06-30 15:43:26 +02:00
committed by GitHub
parent 585295c89f
commit 6edd47dd65
13 changed files with 374 additions and 37 deletions

View File

@@ -80,4 +80,44 @@ describe('CommunityPackagesController', () => {
);
});
});
describe('updatePackage', () => {
it('should use the version from the request body when updating a package', async () => {
const req = mock<NodeRequest.Update>({
body: {
name: 'n8n-nodes-test',
version: '2.0.0',
checksum: 'a893hfdsy7399',
},
user: { id: 'user1' },
});
const previouslyInstalledPackage = mock<InstalledPackages>({
installedNodes: [{ type: 'testNode', latestVersion: 1, name: 'testNode' }],
installedVersion: '1.0.0',
authorName: 'Author',
authorEmail: 'author@example.com',
});
const newInstalledPackage = mock<InstalledPackages>({
installedNodes: [{ type: 'testNode', latestVersion: 1, name: 'testNode' }],
installedVersion: '2.0.0',
authorName: 'Author',
authorEmail: 'author@example.com',
});
communityPackagesService.findInstalledPackage.mockResolvedValue(previouslyInstalledPackage);
communityPackagesService.updatePackage.mockResolvedValue(newInstalledPackage);
const result = await controller.updatePackage(req);
expect(communityPackagesService.updatePackage).toHaveBeenCalledWith(
'n8n-nodes-test',
previouslyInstalledPackage,
'2.0.0',
'a893hfdsy7399',
);
expect(result).toBe(newInstalledPackage);
});
});
});

View File

@@ -247,7 +247,7 @@ export class CommunityPackagesController {
@Patch('/')
@GlobalScope('communityPackage:update')
async updatePackage(req: NodeRequest.Update) {
const { name } = req.body;
const { name, version, checksum } = req.body;
if (!name) {
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
@@ -264,6 +264,8 @@ export class CommunityPackagesController {
const newInstalledPackage = await this.communityPackagesService.updatePackage(
this.communityPackagesService.parseNpmPackageName(name).packageName,
previouslyInstalledPackage,
version,
checksum,
);
// broadcast to connected frontends that node list has been updated

View File

@@ -202,7 +202,11 @@ export declare namespace AnnotationTagsRequest {
export declare namespace NodeRequest {
type GetAll = AuthenticatedRequest;
type Post = AuthenticatedRequest<{}, {}, { name?: string; verify?: boolean; version?: string }>;
type Post = AuthenticatedRequest<
{},
{},
{ name?: string; verify?: boolean; version?: string; checksum?: string }
>;
type Delete = AuthenticatedRequest<{}, {}, {}, { name: string }>;

View File

@@ -349,8 +349,10 @@ export class CommunityPackagesService {
async updatePackage(
packageName: string,
installedPackage: InstalledPackages,
version?: string,
checksum?: string,
): Promise<InstalledPackages> {
return await this.installOrUpdatePackage(packageName, { installedPackage });
return await this.installOrUpdatePackage(packageName, { installedPackage, version, checksum });
}
async removePackage(packageName: string, installedPackage: InstalledPackages): Promise<void> {
@@ -376,9 +378,7 @@ export class CommunityPackagesService {
);
}
private checkInstallPermissions(isUpdate: boolean, checksumProvided: boolean) {
if (isUpdate) return;
private checkInstallPermissions(checksumProvided: boolean) {
if (!this.globalConfig.nodes.communityPackages.unverifiedEnabled && !checksumProvided) {
throw new UnexpectedError('Installation of unverified community packages is forbidden!');
}
@@ -386,15 +386,17 @@ export class CommunityPackagesService {
private async installOrUpdatePackage(
packageName: string,
options: { version?: string; checksum?: string } | { installedPackage: InstalledPackages },
options:
| { version?: string; checksum?: string }
| { installedPackage: InstalledPackages; version?: string; checksum?: string } = {},
) {
const isUpdate = 'installedPackage' in options;
const packageVersion = isUpdate || !options.version ? 'latest' : options.version;
const packageVersion = !options.version ? 'latest' : options.version;
const shouldValidateChecksum = 'checksum' in options && Boolean(options.checksum);
this.checkInstallPermissions(isUpdate, shouldValidateChecksum);
this.checkInstallPermissions(shouldValidateChecksum);
if (!isUpdate && options.checksum) {
if (options.checksum) {
await verifyIntegrity(packageName, packageVersion, this.getNpmRegistry(), options.checksum);
}

View File

@@ -1911,6 +1911,7 @@
"settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel": "Uninstalling",
"settings.communityNodes.confirmModal.update.title": "Update community node package?",
"settings.communityNodes.confirmModal.update.message": "You are about to update {packageName} to version {version}",
"settings.communityNodes.confirmModal.update.warning": "This version has not been verified by n8n and may contain breaking changes or bugs.",
"settings.communityNodes.confirmModal.update.description": "We recommend you deactivate workflows that use any of the package's nodes and reactivate them once the update is completed",
"settings.communityNodes.confirmModal.update.buttonLabel": "Update package",
"settings.communityNodes.confirmModal.update.buttonLoadingLabel": "Updating...",

View File

@@ -27,8 +27,14 @@ export async function uninstallPackage(context: IRestApiContext, name: string):
export async function updatePackage(
context: IRestApiContext,
name: string,
version?: string,
checksum?: string,
): Promise<PublicInstalledPackage> {
return await makeRestApiRequest(context, 'PATCH', '/community-packages', { name });
return await makeRestApiRequest(context, 'PATCH', '/community-packages', {
name,
version,
checksum,
});
}
export async function getAvailableCommunityPackageCount(): Promise<number> {

View File

@@ -46,6 +46,7 @@
"@n8n/utils": "workspace:*",
"@replit/codemirror-indentation-markers": "^6.5.3",
"@sentry/vue": "catalog:frontend",
"@types/semver": "^7.7.0",
"@typescript/vfs": "^1.6.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
@@ -79,6 +80,7 @@
"pinia": "catalog:frontend",
"prettier": "^3.3.3",
"qrcode.vue": "^3.3.4",
"semver": "^7.5.4",
"stream-browserify": "^3.0.0",
"timeago.js": "^4.0.2",
"typescript": "catalog:",

View File

@@ -0,0 +1,45 @@
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import CommunityPackageCard from './CommunityPackageCard.vue';
import { createComponentRenderer } from '@/__tests__/render';
const communityPackage = {
packageName: 'n8n-nodes-test',
installedVersion: '1.0.0',
installedNodes: [{ name: 'TestNode' }],
};
const renderComponent = createComponentRenderer(CommunityPackageCard);
const flushPromises = async () => await new Promise(setImmediate);
describe('CommunityPackageCard', () => {
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
beforeEach(() => {
const pinia = createTestingPinia();
setActivePinia(pinia);
nodeTypesStore = useNodeTypesStore();
});
it('should call nodeTypesStore methods and update latestVerifiedVersion when packageName changes', async () => {
Object.defineProperty(nodeTypesStore, 'visibleNodeTypes', {
get: () => [{ name: 'n8n-nodes-test' }],
});
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
nodeTypesStore.getCommunityNodeAttributes = vi.fn().mockResolvedValue({ npmVersion: '2.0.0' });
renderComponent({
props: {
communityPackage,
},
});
await flushPromises();
expect(nodeTypesStore.loadNodeTypesIfNotLoaded).toHaveBeenCalled();
expect(nodeTypesStore.getCommunityNodeAttributes).toHaveBeenCalledWith('n8n-nodes-test');
});
});

View File

@@ -6,6 +6,9 @@ import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store';
import type { UserAction } from '@n8n/design-system';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { computed, ref, watch } from 'vue';
import semver from 'semver';
interface Props {
communityPackage?: PublicInstalledPackage | null;
@@ -21,7 +24,23 @@ const { openCommunityPackageUpdateConfirmModal, openCommunityPackageUninstallCon
useUIStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const settingsStore = useSettingsStore();
const nodeTypesStore = useNodeTypesStore();
const latestVerifiedVersion = ref<string>();
const currVersion = computed(() => props.communityPackage?.installedVersion || '');
const hasUnverifiedPackagesUpdate = computed(() => {
return settingsStore.isUnverifiedPackagesEnabled && props.communityPackage?.updateAvailable;
});
const hasVerifiedPackageUpdate = computed(() => {
const canUpdate =
latestVerifiedVersion.value && semver.gt(latestVerifiedVersion.value || '', currVersion.value);
return settingsStore.isCommunityNodesFeatureEnabled && canUpdate;
});
const packageActions: Array<UserAction<IUser>> = [
{
@@ -57,6 +76,24 @@ function onUpdateClick() {
if (!props.communityPackage) return;
openCommunityPackageUpdateConfirmModal(props.communityPackage.packageName);
}
watch(
() => props.communityPackage?.packageName,
async (packageName) => {
if (packageName) {
await nodeTypesStore.loadNodeTypesIfNotLoaded();
const nodeType = nodeTypesStore.visibleNodeTypes.find((node) =>
node.name.includes(packageName),
);
const attributes = await nodeTypesStore.getCommunityNodeAttributes(nodeType?.name || '');
if (attributes?.npmVersion) {
latestVerifiedVersion.value = attributes.npmVersion;
}
}
},
{ immediate: true },
);
</script>
<template>
@@ -99,7 +136,7 @@ function onUpdateClick() {
<n8n-icon icon="exclamation-triangle" color="danger" size="large" />
</n8n-tooltip>
<n8n-tooltip
v-else-if="settingsStore.isUnverifiedPackagesEnabled && communityPackage.updateAvailable"
v-else-if="hasUnverifiedPackagesUpdate || hasVerifiedPackageUpdate"
placement="top"
>
<template #content>

View File

@@ -0,0 +1,106 @@
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { createComponentRenderer } from '@/__tests__/render';
import CommunityPackageManageConfirmModal from './CommunityPackageManageConfirmModal.vue';
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '@/__tests__/defaults';
import { mockNodeTypeDescription } from '@/__tests__/mocks';
import { createTestingPinia } from '@pinia/testing';
import { STORES } from '@n8n/stores';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY } from '@/constants';
const renderComponent = createComponentRenderer(CommunityPackageManageConfirmModal, {
data() {
return {
packageName: 'n8n-nodes-hello',
};
},
pinia: createTestingPinia({
initialState: {
[STORES.UI]: {
modalsById: {
[COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY]: { open: true },
},
},
[STORES.COMMUNITY_NODES]: {
installedPackages: {
'n8n-nodes-test': {
packageName: 'n8n-nodes-test',
installedVersion: '1.0.0',
updateAvailable: '2.0.0',
},
},
},
[STORES.NODE_TYPES]: {
nodeTypes: {
['n8n-nodes-test.test']: {
1: mockNodeTypeDescription({
name: 'n8n-nodes-test.test',
}),
},
},
},
[STORES.SETTINGS]: {
...SETTINGS_STORE_DEFAULT_STATE,
settings: {
...SETTINGS_STORE_DEFAULT_STATE.settings,
communityNodesEnabled: true,
},
},
},
}),
});
const flushPromises = async () => await new Promise(setImmediate);
describe('CommunityPackageManageConfirmModal', () => {
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
beforeEach(() => {
createAppModals();
nodeTypesStore = useNodeTypesStore();
});
afterEach(() => {
cleanupAppModals();
});
it('should call nodeTypesStore methods and update latestVerifiedVersion on mount', async () => {
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
nodeTypesStore.getCommunityNodeAttributes = vi.fn().mockResolvedValue({ npmVersion: '2.0.0' });
renderComponent({
props: {
modalName: 'test-modal',
activePackageName: 'n8n-nodes-test',
mode: 'update',
},
});
await flushPromises();
expect(nodeTypesStore.loadNodeTypesIfNotLoaded).toHaveBeenCalled();
expect(nodeTypesStore.getCommunityNodeAttributes).toHaveBeenCalledWith('n8n-nodes-test.test');
});
it('should call nodeTypesStore methods and update latestVerifiedVersion on mount', async () => {
useSettingsStore().setSettings({ ...defaultSettings, communityNodesEnabled: true });
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
nodeTypesStore.getCommunityNodeAttributes = vi.fn().mockResolvedValue({ npmVersion: '1.5.0' });
const { getByTestId } = renderComponent({
props: {
modalName: 'test-modal',
activePackageName: 'n8n-nodes-test',
mode: 'update',
},
});
await flushPromises();
const testId = getByTestId('communityPackageManageConfirmModal-warning');
expect(testId).toBeInTheDocument();
});
});

View File

@@ -6,8 +6,11 @@ import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { createEventBus } from '@n8n/utils/event-bus';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { CommunityNodeType } from '@n8n/api-types';
import { useSettingsStore } from '@/stores/settings.store';
import semver from 'semver';
export type CommunityPackageManageMode = 'uninstall' | 'update' | 'view-documentation';
@@ -20,6 +23,8 @@ interface Props {
const props = defineProps<Props>();
const communityNodesStore = useCommunityNodesStore();
const nodeTypesStore = useNodeTypesStore();
const settingsStore = useSettingsStore();
const modalBus = createEventBus();
@@ -29,9 +34,24 @@ const telemetry = useTelemetry();
const loading = ref(false);
const activePackage = computed(
const isUsingVerifiedAndUnverifiedPackages =
settingsStore.isCommunityNodesFeatureEnabled && settingsStore.isUnverifiedPackagesEnabled;
const isUsingVerifiedPackagesOnly =
settingsStore.isCommunityNodesFeatureEnabled && !settingsStore.isUnverifiedPackagesEnabled;
const communityStorePackage = computed(
() => communityNodesStore.installedPackages[props.activePackageName],
);
const updateVersion = computed(() => {
return settingsStore.isUnverifiedPackagesEnabled
? communityStorePackage.value.updateAvailable
: nodeTypeStorePackage.value?.npmVersion;
});
const nodeTypeStorePackage = ref<CommunityNodeType>();
const isLatestPackageVerified = ref<boolean>(true);
const packageVersion = ref<string>(communityStorePackage.value.updateAvailable ?? '');
const getModalContent = computed(() => {
if (props.mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL) {
@@ -55,10 +75,11 @@ const getModalContent = computed(() => {
},
}),
description: i18n.baseText('settings.communityNodes.confirmModal.update.description'),
warning: i18n.baseText('settings.communityNodes.confirmModal.update.warning'),
message: i18n.baseText('settings.communityNodes.confirmModal.update.message', {
interpolate: {
packageName: props.activePackageName,
version: activePackage.value.updateAvailable ?? '',
version: packageVersion.value,
},
}),
buttonLabel: i18n.baseText('settings.communityNodes.confirmModal.update.buttonLabel'),
@@ -83,11 +104,11 @@ const onConfirmButtonClick = async () => {
const onUninstall = async () => {
try {
telemetry.track('user started cnr package deletion', {
package_name: activePackage.value.packageName,
package_node_names: activePackage.value.installedNodes.map((node) => node.name),
package_version: activePackage.value.installedVersion,
package_author: activePackage.value.authorName,
package_author_email: activePackage.value.authorEmail,
package_name: communityStorePackage.value.packageName,
package_node_names: communityStorePackage.value.installedNodes.map((node) => node.name),
package_version: communityStorePackage.value.installedVersion,
package_author: communityStorePackage.value.authorName,
package_author_email: communityStorePackage.value.authorEmail,
});
loading.value = true;
await communityNodesStore.uninstallPackage(props.activePackageName);
@@ -107,23 +128,34 @@ const onUninstall = async () => {
const onUpdate = async () => {
try {
telemetry.track('user started cnr package update', {
package_name: activePackage.value.packageName,
package_node_names: activePackage.value.installedNodes.map((node) => node.name),
package_version_current: activePackage.value.installedVersion,
package_version_new: activePackage.value.updateAvailable,
package_author: activePackage.value.authorName,
package_author_email: activePackage.value.authorEmail,
package_name: communityStorePackage.value.packageName,
package_node_names: communityStorePackage.value.installedNodes.map((node) => node.name),
package_version_current: communityStorePackage.value.installedVersion,
package_version_new: communityStorePackage.value.updateAvailable,
package_author: communityStorePackage.value.authorName,
package_author_email: communityStorePackage.value.authorEmail,
});
loading.value = true;
const updatedVersion = activePackage.value.updateAvailable;
await communityNodesStore.updatePackage(props.activePackageName);
if (settingsStore.isUnverifiedPackagesEnabled) {
await communityNodesStore.updatePackage(props.activePackageName);
} else if (settingsStore.isCommunityNodesFeatureEnabled) {
await communityNodesStore.updatePackage(
props.activePackageName,
updateVersion.value,
nodeTypeStorePackage.value?.checksum,
);
} else {
throw new Error('Community nodes feature is not correctly enabled.');
}
await useNodeTypesStore().getNodeTypes();
toast.showMessage({
title: i18n.baseText('settings.communityNodes.messages.update.success.title'),
message: i18n.baseText('settings.communityNodes.messages.update.success.message', {
interpolate: {
packageName: props.activePackageName,
version: updatedVersion ?? '',
version: updateVersion.value ?? '',
},
}),
type: 'success',
@@ -135,6 +167,47 @@ const onUpdate = async () => {
modalBus.emit('close');
}
};
async function fetchPackageInfo(packageName: string) {
await nodeTypesStore.loadNodeTypesIfNotLoaded();
const nodeType = nodeTypesStore.visibleNodeTypes.find((nodeType) =>
nodeType.name.includes(packageName),
);
if (nodeType) {
const communityNodeAttributes = await nodeTypesStore.getCommunityNodeAttributes(nodeType?.name);
nodeTypeStorePackage.value = communityNodeAttributes ?? undefined;
}
}
function setIsVerifiedLatestPackage() {
if (
isUsingVerifiedAndUnverifiedPackages &&
nodeTypeStorePackage.value?.npmVersion &&
communityStorePackage.value.updateAvailable
) {
isLatestPackageVerified.value = semver.eq(
nodeTypeStorePackage.value.npmVersion,
communityStorePackage.value.updateAvailable,
);
}
}
function setPackageVersion() {
if (isUsingVerifiedPackagesOnly) {
packageVersion.value = nodeTypeStorePackage.value?.npmVersion ?? packageVersion.value;
}
}
onMounted(async () => {
if (props.activePackageName) {
await fetchPackageInfo(props.activePackageName);
}
setIsVerifiedLatestPackage();
setPackageVersion();
});
</script>
<template>
@@ -156,6 +229,11 @@ const onUpdate = async () => {
<n8n-info-tip theme="info" type="note" :bold="false">
<span v-text="getModalContent.description"></span>
</n8n-info-tip>
<n8n-notice
data-test-id="communityPackageManageConfirmModal-warning"
v-if="!isLatestPackageVerified"
:content="getModalContent.warning"
/>
</div>
</template>
<template #footer>
@@ -175,6 +253,7 @@ const onUpdate = async () => {
.descriptionContainer {
display: flex;
margin: var(--spacing-s) 0;
flex-direction: column;
}
.descriptionIcon {

View File

@@ -81,11 +81,17 @@ export const useCommunityNodesStore = defineStore(STORES.COMMUNITY_NODES, () =>
installedPackages.value[newPackage.packageName] = newPackage;
};
const updatePackage = async (packageName: string): Promise<void> => {
const updatePackage = async (
packageName: string,
version?: string,
checksum?: string,
): Promise<void> => {
const packageToUpdate = installedPackages.value[packageName];
const updatedPackage = await communityNodesApi.updatePackage(
rootStore.restApiContext,
packageToUpdate.packageName,
version,
checksum,
);
updatePackageObject(updatedPackage);
};
@@ -99,5 +105,6 @@ export const useCommunityNodesStore = defineStore(STORES.COMMUNITY_NODES, () =>
installPackage,
uninstallPackage,
updatePackage,
setInstalledPackages,
};
});

20
pnpm-lock.yaml generated
View File

@@ -2261,6 +2261,9 @@ importers:
'@sentry/vue':
specifier: catalog:frontend
version: 8.33.1(vue@3.5.13(typescript@5.8.3))
'@types/semver':
specifier: ^7.7.0
version: 7.7.0
'@typescript/vfs':
specifier: ^1.6.0
version: 1.6.0(typescript@5.8.3)
@@ -2360,6 +2363,9 @@ importers:
qrcode.vue:
specifier: ^3.3.4
version: 3.3.4(vue@3.5.13(typescript@5.8.3))
semver:
specifier: ^7.5.4
version: 7.7.2
stream-browserify:
specifier: ^3.0.0
version: 3.0.0
@@ -6986,8 +6992,8 @@ packages:
'@types/scheduler@0.26.0':
resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==}
'@types/semver@7.5.0':
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
'@types/semver@7.7.0':
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
'@types/send@0.17.4':
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
@@ -20621,7 +20627,7 @@ snapshots:
'@types/scheduler@0.26.0': {}
'@types/semver@7.5.0': {}
'@types/semver@7.7.0': {}
'@types/send@0.17.4':
dependencies:
@@ -20844,7 +20850,7 @@ snapshots:
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.6.0
semver: 7.7.2
ts-api-utils: 1.4.3(typescript@5.8.3)
optionalDependencies:
typescript: 5.8.3
@@ -20871,7 +20877,7 @@ snapshots:
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@1.21.0))
'@types/json-schema': 7.0.15
'@types/semver': 7.5.0
'@types/semver': 7.7.0
'@typescript-eslint/scope-manager': 6.21.0
'@typescript-eslint/types': 6.21.0
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3)
@@ -27065,7 +27071,7 @@ snapshots:
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.6.0
semver: 7.7.2
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.0
@@ -29456,7 +29462,7 @@ snapshots:
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
semver: 7.6.0
semver: 7.7.2
typescript: 5.8.3
yargs-parser: 21.1.1
optionalDependencies: