@aurracloud/mcp-cli
Version:
A command-line tool to install, manage, and setup MCP (Model Context Protocol) servers with Docker support
807 lines • 31 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseGitHubIdentifier = parseGitHubIdentifier;
exports.checkGitHubRepoExists = checkGitHubRepoExists;
exports.cloneRepository = cloneRepository;
exports.cloneRepositoryQuiet = cloneRepositoryQuiet;
exports.readPackageJson = readPackageJson;
exports.checkRepoExecutability = checkRepoExecutability;
exports.checkPythonRepoExecutability = checkPythonRepoExecutability;
exports.cleanupClonedRepo = cleanupClonedRepo;
exports.findMcpServersInRepo = findMcpServersInRepo;
exports.readPackageJsonFromSubdir = readPackageJsonFromSubdir;
exports.readPythonPackageFromSubdir = readPythonPackageFromSubdir;
exports.checkGitRepoExistence = checkGitRepoExistence;
exports.isGitRepoNpxExecutable = isGitRepoNpxExecutable;
exports.isGitRepoUvxExecutable = isGitRepoUvxExecutable;
exports.getGitNpxCommand = getGitNpxCommand;
exports.getGitUvxCommand = getGitUvxCommand;
exports.checkGitRepoExistenceQuiet = checkGitRepoExistenceQuiet;
const node_fetch_1 = __importDefault(require("node-fetch"));
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const path_1 = require("path");
const os_1 = require("os");
const logger_1 = require("./utils/logger");
const utils_1 = require("./lib/utils");
const python_1 = require("./python");
/**
* Parses a GitHub URL or repository identifier to extract owner, repo name, and optional path
*
* @param repoIdentifier - GitHub URL or owner/repo format, optionally with tree/branch/path
* @returns Object with owner, repo name, and optional path, or null if invalid
*/
function parseGitHubIdentifier(repoIdentifier) {
try {
// Remove trailing .git if present
let cleanIdentifier = repoIdentifier.replace(/\.git$/, '');
// Handle npm scoped package format (@owner/repo) by removing the @
if (cleanIdentifier.startsWith('@')) {
cleanIdentifier = cleanIdentifier.substring(1);
}
// Handle SSH format (git@github.com:owner/repo)
if (cleanIdentifier.includes('git@github.com:')) {
const match = cleanIdentifier.match(/git@github\.com:([^\/]+)\/([^\/\s]+)/);
if (match) {
return { owner: match[1], repo: match[2] };
}
}
// Handle full GitHub URLs with potential tree/branch/path
if (cleanIdentifier.includes('github.com')) {
// Match patterns like:
// - https://github.com/owner/repo
// - https://github.com/owner/repo/tree/main/src/path
// - https://github.com/owner/repo/blob/main/src/path
// - https://github.com/owner/repo/tree/branch-name/src/path
const match = cleanIdentifier.match(/github\.com[\/:]([^\/]+)\/([^\/\s]+)(?:\/(tree|blob)\/([^\/]+)\/(.+))?/);
if (match) {
const [, owner, repo, , branch, path] = match;
return {
owner,
repo,
branch: branch || undefined,
path: path || undefined
};
}
}
// Handle owner/repo format
if (cleanIdentifier.includes('/') && !cleanIdentifier.includes(' ')) {
const parts = cleanIdentifier.split('/');
if (parts.length === 2 && parts[0] && parts[1]) {
return { owner: parts[0], repo: parts[1] };
}
}
return null;
}
catch (error) {
return null;
}
}
/**
* Checks if a GitHub repository exists
*
* @param owner - Repository owner
* @param repo - Repository name
* @returns Promise resolving to repository information or null if not found
*/
async function checkGitHubRepoExists(owner, repo) {
try {
const response = await (0, node_fetch_1.default)(`https://api.github.com/repos/${owner}/${repo}`);
if (!response.ok) {
if (response.status === 404) {
return null;
}
const errorText = await response.text();
throw new Error(`Failed to check repository existence: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return {
name: data.name,
fullName: data.full_name,
description: data.description,
htmlUrl: data.html_url,
cloneUrl: data.clone_url,
defaultBranch: data.default_branch,
language: data.language,
topics: data.topics,
license: data.license ? {
name: data.license.name,
spdxId: data.license.spdx_id
} : undefined,
owner: {
login: data.owner.login,
type: data.owner.type
}
};
}
catch (error) {
logger_1.logger.error(`Error checking GitHub repository existence for ${owner}/${repo}`, error);
throw error;
}
}
/**
* Clones a GitHub repository to a temporary directory
*
* @param cloneUrl - The clone URL of the repository
* @param repoName - Name of the repository for the directory
* @returns Promise resolving to the path where the repository was cloned
*/
async function cloneRepository(cloneUrl, repoName) {
const tempDir = (0, os_1.tmpdir)();
const clonePath = (0, path_1.join)(tempDir, `mcp-cli-${repoName}-${Date.now()}`);
try {
logger_1.logger.info(`Cloning repository to ${clonePath}...`);
// Use git clone with depth 1 for faster cloning
(0, child_process_1.execSync)(`git clone --depth 1 "${cloneUrl}" "${clonePath}"`, {
stdio: 'pipe',
timeout: 30000 // 30 second timeout
});
logger_1.logger.success(`Repository cloned successfully to ${clonePath}`);
return clonePath;
}
catch (error) {
logger_1.logger.error(`Failed to clone repository from ${cloneUrl}`, error);
// Clean up on failure
try {
if ((0, fs_1.existsSync)(clonePath)) {
(0, fs_1.rmSync)(clonePath, { recursive: true, force: true });
}
}
catch (cleanupError) {
logger_1.logger.warn(`Failed to clean up failed clone at ${clonePath}`);
}
throw error;
}
}
/**
* Clones a GitHub repository to a temporary directory (with optional JSON mode)
*
* @param cloneUrl - The clone URL of the repository
* @param repoName - Name of the repository for the directory
* @param jsonMode - Whether to suppress logging output (for JSON mode) - deprecated, use setLoggerMode instead
* @returns Promise resolving to the path where the repository was cloned
*/
async function cloneRepositoryQuiet(cloneUrl, repoName, jsonMode = false) {
const tempDir = (0, os_1.tmpdir)();
const clonePath = (0, path_1.join)(tempDir, `mcp-cli-${repoName}-${Date.now()}`);
try {
logger_1.logger.info(`Cloning repository to ${clonePath}...`);
// Use git clone with depth 1 for faster cloning
(0, child_process_1.execSync)(`git clone --depth 1 "${cloneUrl}" "${clonePath}"`, {
stdio: 'pipe',
timeout: 30000 // 30 second timeout
});
logger_1.logger.success(`Repository cloned successfully to ${clonePath}`);
return clonePath;
}
catch (error) {
logger_1.logger.error(`Failed to clone repository from ${cloneUrl}`, error);
// Clean up on failure
try {
if ((0, fs_1.existsSync)(clonePath)) {
(0, fs_1.rmSync)(clonePath, { recursive: true, force: true });
}
}
catch (cleanupError) {
logger_1.logger.warn(`Failed to clean up failed clone at ${clonePath}`);
}
throw error;
}
}
/**
* Reads and parses package.json from a cloned repository
*
* @param clonePath - Path to the cloned repository
* @returns Package information or null if package.json doesn't exist or is invalid
*/
function readPackageJson(clonePath) {
const packageJsonPath = (0, path_1.join)(clonePath, 'package.json');
if (!(0, fs_1.existsSync)(packageJsonPath)) {
return null;
}
try {
const packageJsonContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
const packageInfo = JSON.parse(packageJsonContent);
return packageInfo;
}
catch (error) {
logger_1.logger.error(`Failed to parse package.json at ${packageJsonPath}`, error);
return null;
}
}
/**
* Checks if a repository package is executable (has bin scripts or is a CLI tool)
*
* @param packageInfo - Package information from package.json
* @returns Object with executability information
*/
function checkRepoExecutability(packageInfo) {
// Check if package has bin scripts
if (packageInfo.bin) {
if (typeof packageInfo.bin === 'string') {
// Single binary
return {
isExecutable: true,
executableName: packageInfo.name,
executablePath: packageInfo.bin
};
}
else {
// Multiple binaries - use the first one
const binEntries = Object.entries(packageInfo.bin);
if (binEntries.length > 0) {
const [binName, binPath] = binEntries[0];
return {
isExecutable: true,
executableName: binName,
executablePath: binPath
};
}
}
}
// Check if it's likely a CLI tool based on name patterns
const cliPatterns = ['-cli', 'cli-', '-tool', 'tool-'];
const nameIndicatesCli = cliPatterns.some(pattern => packageInfo.name.toLowerCase().includes(pattern));
if (nameIndicatesCli) {
return {
isExecutable: true,
executableName: packageInfo.name,
executablePath: packageInfo.name
};
}
return { isExecutable: false };
}
/**
* Checks if a Python package is executable (has console scripts or entry points)
*
* @param pythonPackageInfo - Python package information from pyproject.toml
* @returns Object with executability information
*/
function checkPythonRepoExecutability(pythonPackageInfo) {
// Check for console scripts in entry-points
if (pythonPackageInfo['entry-points']?.console_scripts) {
const scripts = Object.entries(pythonPackageInfo['entry-points'].console_scripts);
if (scripts.length > 0) {
const [scriptName, scriptPath] = scripts[0];
return {
isExecutable: true,
executableName: scriptName,
executablePath: scriptPath
};
}
}
// Check for scripts in project.scripts
if (pythonPackageInfo.scripts) {
const scripts = Object.entries(pythonPackageInfo.scripts);
if (scripts.length > 0) {
const [scriptName, scriptPath] = scripts[0];
return {
isExecutable: true,
executableName: scriptName,
executablePath: scriptPath
};
}
}
// Check if package name suggests it's a tool (common patterns)
const toolPatterns = ['-cli', 'cli-', 'tool', 'cmd', 'command'];
const nameIndicatesTool = toolPatterns.some(pattern => pythonPackageInfo.name.toLowerCase().includes(pattern));
if (nameIndicatesTool) {
return {
isExecutable: true,
executableName: pythonPackageInfo.name,
executablePath: pythonPackageInfo.name
};
}
return { isExecutable: false };
}
/**
* Cleans up a cloned repository directory
*
* @param clonePath - Path to the cloned repository to clean up
* @param jsonMode - Whether to suppress logging output (for JSON mode) - deprecated, use setLoggerMode instead
*/
function cleanupClonedRepo(clonePath, jsonMode = false) {
try {
if ((0, fs_1.existsSync)(clonePath)) {
(0, fs_1.rmSync)(clonePath, { recursive: true, force: true });
logger_1.logger.info(`Cleaned up cloned repository at ${clonePath}`);
}
}
catch (error) {
logger_1.logger.warn(`Failed to clean up cloned repository at ${clonePath}: ${error.message || error}`);
}
}
/**
* Finds all MCP servers in a cloned repository
*
* @param clonePath - Path to the cloned repository
* @returns Array of MCP server information
*/
async function findMcpServersInRepo(clonePath) {
const mcpServers = [];
try {
// Find all package.json files with MCP configurations
const mcpConfigPaths = await (0, utils_1.findMcpConfigs)(clonePath, true);
for (const configPath of mcpConfigPaths) {
try {
const packageJsonContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
const packageInfo = JSON.parse(packageJsonContent);
if (packageInfo.mcpServer) {
// Calculate relative path from repository root
const relativePath = configPath.replace(clonePath, '').replace(/^\//, '').replace(/\/package\.json$/, '') || '.';
// Check executability
const { isExecutable, executableName, executablePath } = checkRepoExecutability(packageInfo);
const serverInfo = {
name: packageInfo.name,
path: relativePath,
packageInfo,
runtime: 'node',
isExecutable,
executableName,
executablePath
};
mcpServers.push(serverInfo);
}
}
catch (error) {
logger_1.logger.warn(`Failed to parse package.json at ${configPath}: ${error}`);
}
}
}
catch (error) {
logger_1.logger.warn(`Failed to find MCP configurations in ${clonePath}: ${error}`);
}
return mcpServers;
}
/**
* Reads package.json from a specific subdirectory in a cloned repository
*
* @param clonePath - Path to the cloned repository
* @param subPath - Subdirectory path within the repository
* @returns Package information or null if not found
*/
function readPackageJsonFromSubdir(clonePath, subPath) {
const packageJsonPath = (0, path_1.join)(clonePath, subPath, 'package.json');
if (!(0, fs_1.existsSync)(packageJsonPath)) {
return null;
}
try {
const packageJsonContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
const packageInfo = JSON.parse(packageJsonContent);
return packageInfo;
}
catch (error) {
logger_1.logger.error(`Failed to parse package.json at ${packageJsonPath}`, error);
return null;
}
}
/**
* Reads Python package information from a specific subdirectory in a cloned repository
*
* @param clonePath - Path to the cloned repository
* @param subPath - Subdirectory path within the repository
* @returns Python package information or null if not found
*/
function readPythonPackageFromSubdir(clonePath, subPath) {
const pyprojectPath = (0, path_1.join)(clonePath, subPath, 'pyproject.toml');
if (!(0, fs_1.existsSync)(pyprojectPath)) {
return null;
}
try {
const pyprojectData = (0, python_1.readPyProjectToml)(pyprojectPath);
if (!pyprojectData?.project) {
return null;
}
const project = pyprojectData.project;
return {
name: project.name || '',
version: project.version || '',
description: project.description,
authors: project.authors,
license: project.license,
readme: project.readme,
requires_python: project.requires_python,
classifiers: project.classifiers,
keywords: project.keywords,
dependencies: project.dependencies,
urls: project.urls,
scripts: project.scripts,
'gui-scripts': project['gui-scripts'],
'entry-points': project['entry-points'],
optional_dependencies: project.optional_dependencies
};
}
catch (error) {
logger_1.logger.error(`Failed to parse pyproject.toml at ${pyprojectPath}`, error);
return null;
}
}
/**
* Checks if a GitHub repository exists and whether it's executable via npx
*
* @param repoIdentifier - GitHub URL or owner/repo format
* @returns Promise resolving to repository existence and executability information
*/
async function checkGitRepoExistence(repoIdentifier) {
try {
// Parse the repository identifier
const parsed = parseGitHubIdentifier(repoIdentifier);
if (!parsed) {
return {
exists: false,
isExecutable: false
};
}
const { owner, repo, path, branch } = parsed;
// Check if repository exists on GitHub
const repoInfo = await checkGitHubRepoExists(owner, repo);
if (!repoInfo) {
return {
exists: false,
isExecutable: false
};
}
// Clone the repository
let clonePath;
try {
clonePath = await cloneRepository(repoInfo.cloneUrl, repoInfo.name);
}
catch (error) {
return {
exists: true,
isExecutable: false,
repoInfo
};
}
// If a specific path was provided, check that subdirectory
if (path) {
const packageInfo = readPackageJsonFromSubdir(clonePath, path);
const pythonPackageInfo = readPythonPackageFromSubdir(clonePath, path);
// Check if we found either a Node.js or Python package
if (!packageInfo && !pythonPackageInfo) {
cleanupClonedRepo(clonePath);
return {
exists: true,
isExecutable: false,
repoInfo,
clonePath,
targetPath: path
};
}
let selectedServer;
if (packageInfo) {
// Node.js package found
const { isExecutable, executableName, executablePath } = checkRepoExecutability(packageInfo);
selectedServer = {
name: packageInfo.name,
path: path,
packageInfo,
runtime: 'node',
isExecutable,
executableName,
executablePath
};
}
else if (pythonPackageInfo) {
// Python package found
const { isExecutable, executableName, executablePath } = checkPythonRepoExecutability(pythonPackageInfo);
selectedServer = {
name: pythonPackageInfo.name,
path: path,
pythonPackageInfo,
runtime: 'python',
isExecutable,
executableName,
executablePath
};
}
else {
// This shouldn't happen due to the check above, but for type safety
cleanupClonedRepo(clonePath);
return {
exists: true,
isExecutable: false,
repoInfo,
clonePath,
targetPath: path
};
}
return {
exists: true,
isExecutable: selectedServer.isExecutable,
repoInfo,
packageInfo: selectedServer.packageInfo, // For backward compatibility
executableName: selectedServer.executableName,
executablePath: selectedServer.executablePath,
clonePath,
targetPath: path,
selectedServer
};
}
// No specific path provided - check root and find all MCP servers
const rootPackageInfo = readPackageJson(clonePath);
const mcpServers = await findMcpServersInRepo(clonePath);
// For backward compatibility, use root package.json if it exists
let isExecutable = false;
let executableName;
let executablePath;
if (rootPackageInfo) {
const executability = checkRepoExecutability(rootPackageInfo);
isExecutable = executability.isExecutable;
executableName = executability.executableName;
executablePath = executability.executablePath;
}
return {
exists: true,
isExecutable,
repoInfo,
packageInfo: rootPackageInfo || undefined,
executableName,
executablePath,
clonePath,
mcpServers
};
}
catch (error) {
logger_1.logger.error(`Error checking git repository existence for ${repoIdentifier}`, error);
throw error;
}
}
/**
* Verifies if a GitHub repository can be installed and executed via npx
*
* @param repoIdentifier - GitHub URL or owner/repo format
* @returns Promise resolving to a boolean indicating if the repository is npx-executable
*/
async function isGitRepoNpxExecutable(repoIdentifier) {
try {
const result = await checkGitRepoExistence(repoIdentifier);
if (!result.exists) {
logger_1.logger.warn(`Repository ${repoIdentifier} does not exist on GitHub`);
return false;
}
if (!result.packageInfo) {
logger_1.logger.warn(`Repository ${repoIdentifier} exists but does not have a package.json`);
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
return false;
}
if (!result.isExecutable) {
logger_1.logger.warn(`Repository ${repoIdentifier} has package.json but is not executable`);
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
return false;
}
return true;
}
catch (error) {
logger_1.logger.error(`Error checking if ${repoIdentifier} is npx-executable`, error);
return false;
}
}
/**
* Verifies if a GitHub repository can be installed and executed via uvx (Python)
*
* @param repoIdentifier - GitHub URL or owner/repo format
* @returns Promise resolving to a boolean indicating if the repository is uvx-executable
*/
async function isGitRepoUvxExecutable(repoIdentifier) {
try {
const result = await checkGitRepoExistence(repoIdentifier);
if (!result.exists) {
logger_1.logger.warn(`Repository ${repoIdentifier} does not exist on GitHub`);
return false;
}
// Check if it's a Python package (either selected server or has Python package files)
if (result.selectedServer?.runtime === 'python' && result.selectedServer.isExecutable) {
return true;
}
// For repositories without specific path, check if it has Python package files
if (!result.selectedServer && result.clonePath) {
const hasPythonFiles = (0, python_1.hasPythonPackageFiles)(result.clonePath);
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
return hasPythonFiles;
}
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
return false;
}
catch (error) {
logger_1.logger.error(`Error checking if ${repoIdentifier} is uvx-executable`, error);
return false;
}
}
/**
* Gets the recommended npx command for a GitHub repository
*
* @param repoIdentifier - GitHub URL or owner/repo format
* @returns Promise resolving to the npx command structure or null if not executable
*/
async function getGitNpxCommand(repoIdentifier) {
try {
const result = await checkGitRepoExistence(repoIdentifier);
if (!result.exists || !result.isExecutable || !result.packageInfo) {
return null;
}
// Use the executable name if available, otherwise use package name
const executableName = result.executableName || result.packageInfo.name;
// Clean up the cloned repository since we only needed to check
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
return {
command: 'npx',
args: ['-y', repoIdentifier]
};
}
catch (error) {
logger_1.logger.error(`Error getting npx command for ${repoIdentifier}`, error);
return null;
}
}
/**
* Gets the recommended uvx command for a GitHub repository (Python)
*
* @param repoIdentifier - GitHub URL or owner/repo format
* @returns Promise resolving to the uvx command structure or null if not executable
*/
async function getGitUvxCommand(repoIdentifier) {
try {
const result = await checkGitRepoExistence(repoIdentifier);
if (!result.exists || !result.selectedServer || result.selectedServer.runtime !== 'python' || !result.selectedServer.isExecutable) {
return null;
}
// Use the executable name if available, otherwise use package name
const executableName = result.selectedServer.executableName || result.selectedServer.pythonPackageInfo?.name;
// Clean up the cloned repository since we only needed to check
if (result.clonePath) {
cleanupClonedRepo(result.clonePath);
}
if (!executableName) {
return null;
}
return {
command: 'uvx',
args: [executableName]
};
}
catch (error) {
logger_1.logger.error(`Error getting uvx command for ${repoIdentifier}`, error);
return null;
}
}
/**
* Checks if a GitHub repository exists and can be cloned (quiet version for JSON mode)
*
* @param repoIdentifier - GitHub URL, owner/repo, or @owner/repo format
* @param jsonMode - Whether to suppress logging output
* @returns Promise resolving to repository existence information
*/
async function checkGitRepoExistenceQuiet(repoIdentifier, jsonMode = false) {
try {
const parsedIdentifier = parseGitHubIdentifier(repoIdentifier);
if (!parsedIdentifier) {
return {
exists: false,
isExecutable: false
};
}
const { owner, repo, path: targetPath } = parsedIdentifier;
// Check if repository exists on GitHub
const repoInfo = await checkGitHubRepoExists(owner, repo);
if (!repoInfo) {
return {
exists: false,
isExecutable: false
};
}
// Clone the repository to check its contents
let clonePath;
try {
clonePath = await cloneRepositoryQuiet(repoInfo.cloneUrl, repoInfo.name, jsonMode);
}
catch (error) {
return {
exists: true,
repoInfo,
isExecutable: false
};
}
// If a specific path was provided, check that path
if (targetPath && targetPath !== '.') {
const serverPath = (0, path_1.join)(clonePath, targetPath);
if (!(0, fs_1.existsSync)(serverPath)) {
cleanupClonedRepo(clonePath);
return {
exists: true,
repoInfo,
isExecutable: false,
targetPath
};
}
// Check if it's a Node.js package
const packageInfo = readPackageJsonFromSubdir(clonePath, targetPath);
if (packageInfo) {
const execInfo = checkRepoExecutability(packageInfo);
return {
exists: true,
repoInfo,
clonePath,
targetPath,
isExecutable: execInfo.isExecutable,
executableName: execInfo.executableName,
executablePath: execInfo.executablePath,
selectedServer: {
name: packageInfo.name,
path: targetPath,
packageInfo,
isExecutable: execInfo.isExecutable,
executableName: execInfo.executableName,
executablePath: execInfo.executablePath,
runtime: 'node'
}
};
}
// Check if it's a Python package
const pythonPackageInfo = readPythonPackageFromSubdir(clonePath, targetPath);
if (pythonPackageInfo) {
const execInfo = checkPythonRepoExecutability(pythonPackageInfo);
return {
exists: true,
repoInfo,
clonePath,
targetPath,
isExecutable: execInfo.isExecutable,
executableName: execInfo.executableName,
selectedServer: {
name: pythonPackageInfo.name,
path: targetPath,
pythonPackageInfo,
isExecutable: execInfo.isExecutable,
executableName: execInfo.executableName,
runtime: 'python'
}
};
}
// Path exists but no package files found
cleanupClonedRepo(clonePath);
return {
exists: true,
repoInfo,
isExecutable: false,
targetPath
};
}
// No specific path provided, check the repository structure
const packageInfo = readPackageJson(clonePath);
// Find all MCP servers in the repository
const mcpServers = await findMcpServersInRepo(clonePath);
// Check if it's executable (has bin field)
const execInfo = packageInfo ? checkRepoExecutability(packageInfo) : { isExecutable: false };
return {
exists: true,
repoInfo,
packageInfo: packageInfo || undefined,
clonePath,
isExecutable: execInfo.isExecutable,
executableName: execInfo.executableName,
executablePath: execInfo.executablePath,
mcpServers: mcpServers.length > 0 ? mcpServers : undefined
};
}
catch (error) {
return {
exists: false,
isExecutable: false
};
}
}
//# sourceMappingURL=git.js.map