mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: add git pull functionality to runTask before opening VSCode (#185)
This commit is contained in:
parent
0b5812b01c
commit
495d1bdf51
6 changed files with 519 additions and 0 deletions
|
|
@ -31,6 +31,7 @@
|
|||
"p-wait-for": "^5.0.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"simple-git": "^3.28.0",
|
||||
"zod": "^3.25.41"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
309
apps/roomote/src/lib/__tests__/gitUtils.test.ts
Normal file
309
apps/roomote/src/lib/__tests__/gitUtils.test.ts
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import {
|
||||
gitPullRepo,
|
||||
gitPullRepoFromConfig,
|
||||
gitPullAllRepos,
|
||||
} from '../gitUtils';
|
||||
import { Logger } from '../logger';
|
||||
import type { RepoConfig } from '../repoConfig';
|
||||
|
||||
// Mock simple-git
|
||||
const mockGit = {
|
||||
status: vi.fn(),
|
||||
revparse: vi.fn(),
|
||||
pull: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('simple-git', () => ({
|
||||
default: vi.fn(() => mockGit),
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('node:fs');
|
||||
vi.mock('../utils', () => ({
|
||||
findGitRoot: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockFs = vi.mocked(fs);
|
||||
const mockFindGitRoot = vi.mocked(await import('../utils')).findGitRoot;
|
||||
const mockSimpleGit = vi.mocked(await import('simple-git')).default;
|
||||
|
||||
describe('gitUtils', () => {
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLogger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
log: vi.fn(),
|
||||
close: vi.fn(),
|
||||
logStream: undefined,
|
||||
logFilePath: '/test/log.txt',
|
||||
tag: 'test',
|
||||
initializeLogger: vi.fn(),
|
||||
writeToLog: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
});
|
||||
|
||||
describe('gitPullRepo', () => {
|
||||
it('should successfully pull from a clean repository', async () => {
|
||||
const repoPath = '/test/repo';
|
||||
const gitRoot = '/test/repo';
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockReturnValue(gitRoot);
|
||||
|
||||
// Mock git status (clean)
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
|
||||
// Mock git branch
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
|
||||
// Mock git pull
|
||||
mockGit.pull.mockResolvedValue({
|
||||
summary: {
|
||||
changes: 1,
|
||||
insertions: 5,
|
||||
deletions: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullRepo(repoPath, mockLogger);
|
||||
|
||||
expect(mockSimpleGit).toHaveBeenCalledWith(gitRoot);
|
||||
expect(mockGit.status).toHaveBeenCalled();
|
||||
expect(mockGit.revparse).toHaveBeenCalledWith(['--abbrev-ref', 'HEAD']);
|
||||
expect(mockGit.pull).toHaveBeenCalled();
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Git pull completed successfully',
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn about uncommitted changes but continue', async () => {
|
||||
const repoPath = '/test/repo';
|
||||
const gitRoot = '/test/repo';
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockReturnValue(gitRoot);
|
||||
|
||||
// Mock git status (dirty)
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => false,
|
||||
files: [{ index: ' ', working_dir: 'M', path: 'file.txt' }],
|
||||
});
|
||||
|
||||
// Mock git branch
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
|
||||
// Mock git pull
|
||||
mockGit.pull.mockResolvedValue({
|
||||
summary: {
|
||||
changes: 0,
|
||||
insertions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullRepo(repoPath, mockLogger);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Repository has uncommitted changes'),
|
||||
);
|
||||
expect(mockGit.pull).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if repository path does not exist', async () => {
|
||||
const repoPath = '/nonexistent/repo';
|
||||
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
|
||||
await expect(gitPullRepo(repoPath, mockLogger)).rejects.toThrow(
|
||||
'Repository path does not exist: /nonexistent/repo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if git pull fails', async () => {
|
||||
const repoPath = '/test/repo';
|
||||
const gitRoot = '/test/repo';
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockReturnValue(gitRoot);
|
||||
|
||||
// Mock git status (clean)
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
|
||||
// Mock git branch
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
|
||||
// Mock git pull failure
|
||||
mockGit.pull.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(gitPullRepo(repoPath, mockLogger)).rejects.toThrow(
|
||||
'Git pull failed for /test/repo: Network error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle repository already up to date', async () => {
|
||||
const repoPath = '/test/repo';
|
||||
const gitRoot = '/test/repo';
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockReturnValue(gitRoot);
|
||||
|
||||
// Mock git status (clean)
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
|
||||
// Mock git branch
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
|
||||
// Mock git pull with no changes
|
||||
mockGit.pull.mockResolvedValue({
|
||||
summary: {
|
||||
changes: 0,
|
||||
insertions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullRepo(repoPath, mockLogger);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Repository is already up to date',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitPullRepoFromConfig', () => {
|
||||
it('should pull repository using config', async () => {
|
||||
const repoConfig: RepoConfig = {
|
||||
name: 'Test Repo',
|
||||
path: '/test/repo',
|
||||
defaultBranch: 'main',
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockReturnValue('/test/repo');
|
||||
|
||||
// Mock successful git operations
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
mockGit.pull.mockResolvedValue({
|
||||
summary: {
|
||||
changes: 0,
|
||||
insertions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullRepoFromConfig(repoConfig, mockLogger);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Updating repository: Test Repo (/test/repo)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitPullAllRepos', () => {
|
||||
it('should pull all repositories successfully', async () => {
|
||||
const repoConfigs: RepoConfig[] = [
|
||||
{
|
||||
name: 'Repo 1',
|
||||
path: '/test/repo1',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
{
|
||||
name: 'Repo 2',
|
||||
path: '/test/repo2',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
];
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockImplementation((path) => path);
|
||||
|
||||
// Mock successful git operations
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
mockGit.pull.mockResolvedValue({
|
||||
summary: {
|
||||
changes: 0,
|
||||
insertions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullAllRepos(repoConfigs, mockLogger);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Pulling latest changes for all 2 configured repositories',
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Completed pulling all configured repositories',
|
||||
);
|
||||
expect(mockSimpleGit).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should continue with other repositories if one fails', async () => {
|
||||
const repoConfigs: RepoConfig[] = [
|
||||
{
|
||||
name: 'Repo 1',
|
||||
path: '/test/repo1',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
{
|
||||
name: 'Repo 2',
|
||||
path: '/test/repo2',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
];
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFindGitRoot.mockImplementation((path) => path);
|
||||
|
||||
// Mock first repo to fail, second to succeed
|
||||
mockGit.status.mockResolvedValue({
|
||||
isClean: () => true,
|
||||
files: [],
|
||||
});
|
||||
mockGit.revparse.mockResolvedValue('main');
|
||||
mockGit.pull
|
||||
.mockRejectedValueOnce(new Error('Network error'))
|
||||
.mockResolvedValueOnce({
|
||||
summary: {
|
||||
changes: 0,
|
||||
insertions: 0,
|
||||
deletions: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await gitPullAllRepos(repoConfigs, mockLogger);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Failed to pull repository Repo 1: Git pull failed for /test/repo1: Network error',
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Completed pulling all configured repositories',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
108
apps/roomote/src/lib/gitUtils.ts
Normal file
108
apps/roomote/src/lib/gitUtils.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import simpleGit from 'simple-git';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import type { Logger } from './logger';
|
||||
import { findGitRoot } from './utils';
|
||||
import type { RepoConfig } from './repoConfig';
|
||||
|
||||
/**
|
||||
* Performs a git pull operation on the specified repository
|
||||
* @param repoPath - The path to the repository
|
||||
* @param logger - Logger instance for logging operations
|
||||
* @returns Promise that resolves when git pull is complete
|
||||
*/
|
||||
export const gitPullRepo = async (
|
||||
repoPath: string,
|
||||
logger?: Logger,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Verify the path exists
|
||||
if (!fs.existsSync(repoPath)) {
|
||||
throw new Error(`Repository path does not exist: ${repoPath}`);
|
||||
}
|
||||
|
||||
// Find the git root to ensure we're in a git repository
|
||||
const gitRoot = findGitRoot(repoPath);
|
||||
logger?.info(`Found git repository at: ${gitRoot}`);
|
||||
|
||||
// Initialize simple-git with the repository path
|
||||
const git = simpleGit(gitRoot);
|
||||
|
||||
// Check if we're in a clean state (no uncommitted changes)
|
||||
const status = await git.status();
|
||||
|
||||
if (!status.isClean()) {
|
||||
logger?.warn(`Repository has uncommitted changes: ${gitRoot}`);
|
||||
logger?.warn(
|
||||
'Uncommitted changes:',
|
||||
status.files
|
||||
.map((f) => `${f.index}${f.working_dir} ${f.path}`)
|
||||
.join('\n'),
|
||||
);
|
||||
// Continue with pull anyway, but log the warning
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const currentBranch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
||||
logger?.info(`Current branch: ${currentBranch}`);
|
||||
|
||||
// Perform git pull
|
||||
logger?.info(`Pulling latest changes for ${gitRoot}...`);
|
||||
const pullResult = await git.pull();
|
||||
|
||||
logger?.info(`Git pull completed successfully`);
|
||||
if (pullResult.summary.changes) {
|
||||
logger?.info(
|
||||
`Git pull summary: ${pullResult.summary.changes} changes, ${pullResult.summary.insertions} insertions, ${pullResult.summary.deletions} deletions`,
|
||||
);
|
||||
} else {
|
||||
logger?.info('Repository is already up to date');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger?.error(`Failed to pull repository ${repoPath}: ${errorMessage}`);
|
||||
throw new Error(`Git pull failed for ${repoPath}: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs git pull on a repository using its configuration
|
||||
* @param repoConfig - Repository configuration
|
||||
* @param logger - Logger instance for logging operations
|
||||
*/
|
||||
export const gitPullRepoFromConfig = async (
|
||||
repoConfig: RepoConfig,
|
||||
logger?: Logger,
|
||||
): Promise<void> => {
|
||||
logger?.info(`Updating repository: ${repoConfig.name} (${repoConfig.path})`);
|
||||
await gitPullRepo(repoConfig.path, logger);
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs git pull on all configured repositories
|
||||
* @param repoConfigs - Array of repository configurations
|
||||
* @param logger - Logger instance for logging operations
|
||||
*/
|
||||
export const gitPullAllRepos = async (
|
||||
repoConfigs: RepoConfig[],
|
||||
logger?: Logger,
|
||||
): Promise<void> => {
|
||||
logger?.info(
|
||||
`Pulling latest changes for all ${repoConfigs.length} configured repositories`,
|
||||
);
|
||||
|
||||
for (const repoConfig of repoConfigs) {
|
||||
try {
|
||||
await gitPullRepoFromConfig(repoConfig, logger);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
logger?.error(
|
||||
`Failed to pull repository ${repoConfig.name}: ${errorMessage}`,
|
||||
);
|
||||
// Continue with other repositories even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
logger?.info('Completed pulling all configured repositories');
|
||||
};
|
||||
50
apps/roomote/src/lib/repoConfig.ts
Normal file
50
apps/roomote/src/lib/repoConfig.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Configuration for available repositories
|
||||
*/
|
||||
export interface RepoConfig {
|
||||
/** Human-readable name of the repository */
|
||||
name: string;
|
||||
/** Absolute path to the repository */
|
||||
path: string;
|
||||
/** Git remote URL (optional, for validation) */
|
||||
remoteUrl?: string;
|
||||
/** Default branch name (defaults to 'main') */
|
||||
defaultBranch?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Available repositories configuration
|
||||
*/
|
||||
export const REPO_CONFIGS: Record<string, RepoConfig> = {
|
||||
'roo-code': {
|
||||
name: 'Roo Code',
|
||||
path: '/roo/repos/Roo-Code',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
'roo-code-cloud': {
|
||||
name: 'Roo Code Cloud',
|
||||
path: '/roo/repos/Roo-Code-Cloud',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get repository configuration by path
|
||||
*/
|
||||
export const getRepoConfigByPath = (path: string): RepoConfig | undefined => {
|
||||
return Object.values(REPO_CONFIGS).find((config) => config.path === path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get repository configuration by key
|
||||
*/
|
||||
export const getRepoConfig = (key: string): RepoConfig | undefined => {
|
||||
return REPO_CONFIGS[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all available repository paths
|
||||
*/
|
||||
export const getAllRepoPaths = (): string[] => {
|
||||
return Object.values(REPO_CONFIGS).map((config) => config.path);
|
||||
};
|
||||
|
|
@ -21,6 +21,8 @@ import type { JobPayload, JobType } from '@roo-code-cloud/db';
|
|||
import { Logger } from './logger';
|
||||
import { isFlyMachine, isDockerContainer } from './utils';
|
||||
import { SlackNotifier } from './slack';
|
||||
import { getRepoConfigByPath, REPO_CONFIGS } from './repoConfig';
|
||||
import { gitPullRepoFromConfig, gitPullAllRepos } from './gitUtils';
|
||||
|
||||
const TIMEOUT = 30 * 60 * 1_000;
|
||||
|
||||
|
|
@ -92,6 +94,27 @@ export const runTask = async <T extends JobType>({
|
|||
|
||||
logger.info(codeCommand);
|
||||
|
||||
// Pull latest changes from git before opening VSCode
|
||||
try {
|
||||
const repoConfig = getRepoConfigByPath(workspacePath);
|
||||
if (repoConfig) {
|
||||
logger.info(`Pulling latest changes for repository: ${repoConfig.name}`);
|
||||
await gitPullRepoFromConfig(repoConfig, logger);
|
||||
} else {
|
||||
logger.warn(
|
||||
`No repository configuration found for path: ${workspacePath}`,
|
||||
);
|
||||
logger.info('Pulling latest changes for all configured repositories');
|
||||
const allRepoConfigs = Object.values(REPO_CONFIGS);
|
||||
await gitPullAllRepos(allRepoConfigs, logger);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Failed to pull git changes: ${errorMessage}`);
|
||||
// Continue with task execution even if git pull fails
|
||||
logger.info('Continuing with task execution despite git pull failure');
|
||||
}
|
||||
|
||||
const subprocess = execa({
|
||||
shell: '/bin/bash',
|
||||
cwd: workspacePath,
|
||||
|
|
|
|||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
|
|
@ -78,6 +78,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
simple-git:
|
||||
specifier: ^3.28.0
|
||||
version: 3.28.0
|
||||
zod:
|
||||
specifier: ^3.25.41
|
||||
version: 3.25.41
|
||||
|
|
@ -1351,6 +1354,12 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1':
|
||||
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
|
||||
|
||||
'@libsql/client-wasm@0.15.5':
|
||||
resolution: {integrity: sha512-JPjnGnLGQu36SPDskXSgyLoVBA0/IgcEC52MKnIa7/rKGIY1I4WKMIYrl2hkbw7+xYzWrGk6Vr8AZ9PeNEtgVA==}
|
||||
bundledDependencies:
|
||||
|
|
@ -6140,6 +6149,9 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-git@3.28.0:
|
||||
resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
|
|
@ -7755,6 +7767,14 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1': {}
|
||||
|
||||
'@libsql/client-wasm@0.15.5':
|
||||
dependencies:
|
||||
'@libsql/core': 0.15.9
|
||||
|
|
@ -13194,6 +13214,14 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-git@3.28.0:
|
||||
dependencies:
|
||||
'@kwsites/file-exists': 1.1.1
|
||||
'@kwsites/promise-deferred': 1.1.1
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue