Skip to content

Commit

Permalink
Merge pull request #55 from michaellatman/devin/1734063549-fix-update…
Browse files Browse the repository at this point in the history
…-command

fix: implement update command and improve error handling
  • Loading branch information
michaellatman authored Dec 15, 2024
2 parents e239a5b + 981076e commit 2d18fbf
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 17 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test:list": "node --loader ts-node/esm src/index.ts list",
"test:install": "node --loader ts-node/esm src/index.ts install",
"test:installed": "node --loader ts-node/esm src/index.ts installed",
"test:update": "node --loader ts-node/esm src/index.ts update",
"extract": "node --loader ts-node/esm src/extractors/modelcontextprotocol-extractor.ts",
"test:uninstall": "node --loader ts-node/esm src/index.ts uninstall",
"version:patch": "npm version patch",
Expand Down
134 changes: 134 additions & 0 deletions src/__tests__/auto-update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { jest } from '@jest/globals';
import { describe, it, expect, beforeEach } from '@jest/globals';
import type { ExecOptions, ChildProcess, ExecException } from 'child_process';

// Type definitions
type ExecResult = { stdout: string; stderr: string };

// Setup mocks
const mockExecPromise = jest.fn().mockName('execPromise') as jest.MockedFunction<
(command: string) => Promise<ExecResult>
>;

// Create a properly typed mock for exec
const mockExec = jest.fn((
command: string,
options: ExecOptions | undefined | null,
callback?: (error: ExecException | null, stdout: string, stderr: string) => void
): ChildProcess => {
return {
on: jest.fn(),
stdout: { on: jest.fn() },
stderr: { on: jest.fn() }
} as unknown as ChildProcess;
});

// Mock chalk module
await jest.unstable_mockModule('chalk', () => ({
default: {
yellow: jest.fn(str => str),
cyan: jest.fn(str => str),
green: jest.fn(str => str),
red: jest.fn(str => str),
}
}));

// Mock child_process module
await jest.unstable_mockModule('child_process', () => ({
exec: mockExec
}));

// Mock util module
await jest.unstable_mockModule('util', () => ({
promisify: jest.fn(() => mockExecPromise)
}));

// Mock fs module
await jest.unstable_mockModule('fs', () => ({
readFileSync: jest.fn(() => JSON.stringify({ version: '1.0.48' }))
}));

// Import after mocking
const { updatePackage } = await import('../auto-update.js');

// Helper to create exec result
const createExecResult = (stdout: string, stderr: string = ''): ExecResult => ({ stdout, stderr });

describe('updatePackage', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should check for updates and install if available', async () => {
mockExecPromise
.mockResolvedValueOnce(createExecResult('1.0.50\n'))
.mockResolvedValueOnce(createExecResult('success'));

await updatePackage();

expect(mockExecPromise).toHaveBeenNthCalledWith(1, 'npm show @michaellatman/mcp-get version');
expect(mockExecPromise).toHaveBeenNthCalledWith(2, 'npm install -g @michaellatman/mcp-get@latest');

expect(console.log).toHaveBeenNthCalledWith(1,
'\nA new version of mcp-get is available: 1.0.50 (current: 1.0.48)'
);
expect(console.log).toHaveBeenNthCalledWith(2,
'Installing update...'
);
expect(console.log).toHaveBeenNthCalledWith(3,
'success'
);
expect(console.log).toHaveBeenNthCalledWith(4,
'✓ Update complete\n'
);
});

it('should handle version check errors gracefully', async () => {
const error = new Error('Failed to check version');
mockExecPromise.mockRejectedValueOnce(error);

await updatePackage();

expect(console.error).toHaveBeenCalledWith(
'Failed to check for updates:',
'Failed to check version'
);
});

it('should handle installation errors gracefully', async () => {
mockExecPromise
.mockResolvedValueOnce(createExecResult('1.0.50\n'))
.mockRejectedValueOnce(new Error('Installation failed'));

await updatePackage();

expect(console.error).toHaveBeenCalledWith(
'Failed to install update:',
'Installation failed'
);
});

describe('silent mode', () => {
it('should not log messages in silent mode when update is available', async () => {
mockExecPromise
.mockResolvedValueOnce(createExecResult('1.0.50\n'))
.mockResolvedValueOnce(createExecResult('success'));

await updatePackage(true);
expect(console.log).not.toHaveBeenCalled();
});

it('should not log errors in silent mode on failure', async () => {
mockExecPromise.mockRejectedValueOnce(new Error('Failed to check version'));

await updatePackage(true);
expect(console.error).not.toHaveBeenCalled();
});
});
});
49 changes: 34 additions & 15 deletions src/auto-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import chalk from 'chalk';
const execAsync = promisify(exec);

async function getCurrentVersion(): Promise<string> {
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
const packageJsonPath = new URL('../package.json', import.meta.url).pathname;
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
}

Expand All @@ -16,26 +17,44 @@ async function getLatestVersion(): Promise<string> {
return stdout.trim();
}

export async function updatePackage(): Promise<void> {
export async function updatePackage(silent: boolean = false): Promise<void> {
try {
const currentVersion = await getCurrentVersion();
const latestVersion = await getLatestVersion();

if (currentVersion !== latestVersion) {
console.log(chalk.yellow(`\nA new version of mcp-get is available: ${latestVersion} (current: ${currentVersion})`));
console.log(chalk.cyan('Installing update...'));

// Use npx to ensure we get the latest version
await execAsync('npx --yes @michaellatman/mcp-get@latest');

console.log(chalk.green('✓ Update complete\n'));

// Exit after update to ensure the new version is used
process.exit(0);
if (!silent) {
console.log(chalk.yellow(`\nA new version of mcp-get is available: ${latestVersion} (current: ${currentVersion})`));
console.log(chalk.cyan('Installing update...'));
}

try {
const { stdout, stderr } = await execAsync('npm install -g @michaellatman/mcp-get@latest');
if (!silent) {
if (stdout) console.log(stdout);
if (stderr) console.error(chalk.yellow('Update process output:'), stderr);
console.log(chalk.green('✓ Update complete\n'));
}
} catch (installError: any) {
if (!silent) {
console.error(chalk.red('Failed to install update:'), installError.message);
if (installError.stdout) console.log('stdout:', installError.stdout);
if (installError.stderr) console.error('stderr:', installError.stderr);
console.error(chalk.yellow('Try running the update manually with sudo:'));
console.error(chalk.cyan(' sudo npm install -g @michaellatman/mcp-get@latest'));
}
return;
}
} else {
if (!silent) console.log(chalk.green('✓ mcp-get is already up to date\n'));
}
} catch (error: any) {
if (!silent) {
console.error(chalk.red('Failed to check for updates:'), error.message);
if (error.stdout) console.log('stdout:', error.stdout);
if (error.stderr) console.error('stderr:', error.stderr);
}
} catch (error) {
// Log update check failure but continue with execution
console.log(chalk.yellow('\nFailed to check for updates. Continuing with current version.'));
return;
}
}

5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { list } from './commands/list.js';
import { install } from './commands/install.js';
import { uninstall } from './commands/uninstall.js';
import { listInstalledPackages } from './commands/installed.js';
import { updatePackage } from './auto-update.js';

const command = process.argv[2];
const packageName = process.argv[3];
Expand All @@ -26,12 +27,16 @@ async function main() {
case 'installed':
await listInstalledPackages();
break;
case 'update':
await updatePackage();
break;
default:
console.log('Available commands:');
console.log(' list List all available packages');
console.log(' install <package> Install a package');
console.log(' uninstall [package] Uninstall a package');
console.log(' installed List installed packages');
console.log(' update Update mcp-get to latest version');
process.exit(1);
}
}
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
Expand Down

0 comments on commit 2d18fbf

Please sign in to comment.