@debugg-ai/cli
Version:
CLI tool for running DebuggAI tests in CI/CD environments
600 lines • 29.2 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const dotenv_1 = require("dotenv");
const test_manager_1 = require("./lib/test-manager");
const workflow_orchestrator_1 = require("./lib/workflow-orchestrator");
const system_logger_1 = require("./util/system-logger");
const crypto_1 = require("crypto");
const telemetry_1 = require("./services/telemetry");
// Load environment variables
(0, dotenv_1.config)();
const program = new commander_1.Command();
program
.name('@debugg-ai/cli')
.description('CLI tool for running DebuggAI tests in CI/CD environments')
.version('1.0.0');
program
.command('test')
.description('Run E2E tests based on git changes')
.option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)')
.option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)')
.option('-r, --repo-path <path>', 'Repository path (default: current directory)')
.option('-o, --output-dir <dir>', 'Test output directory (default: tests/debugg-ai)')
.option('-c, --commit <hash>', 'Specific commit hash to analyze (instead of working changes)')
.option('--commit-range <range>', 'Commit range to analyze (e.g., HEAD~3..HEAD, main..feature-branch)')
.option('--since <date>', 'Analyze commits since date/time (e.g., "2024-01-01", "2 days ago")')
.option('--last <number>', 'Analyze last N commits (e.g., --last 3)')
.option('--pr <number>', 'PR number for GitHub App-based testing (requires GitHub App integration)')
.option('--pr-sequence', 'Enable PR commit sequence testing (sends individual test requests for each commit in PR)')
.option('--base-branch <branch>', 'Base branch for PR testing (auto-detected from GitHub env if not provided)')
.option('--head-branch <branch>', 'Head branch for PR testing (auto-detected from GitHub env if not provided)')
.option('--wait-for-server', 'Wait for local development server to be ready')
.option('--server-port <port>', 'Local server port to wait for (default: 3000)', '3000')
.option('--server-timeout <ms>', 'Server wait timeout in milliseconds (default: 60000)', '60000')
.option('--max-test-time <ms>', 'Maximum test wait time in milliseconds (default: 600000)', '600000')
.option('--tunnel-uuid <uuid>', 'Create ngrok tunnel with custom UUID endpoint (e.g., <uuid>.debugg.ai)')
.option('--tunnel-port <port>', 'Port to tunnel (default: 3000)', '3000')
.option('--download-artifacts', 'Download test artifacts (scripts, recordings, JSON results) to local filesystem')
.option('-v, --verbose', 'Enable verbose logging for debugging')
.option('--dev', 'Enable development logging (shows all technical details, tunnel info, API calls, git details, timing)')
.option('--no-color', 'Disable colored output')
.action(async (options) => {
try {
// Track command start
telemetry_1.telemetry.trackCommandStart('test', options);
// Set up logging mode based on flags
if (options.dev) {
system_logger_1.systemLogger.setDevMode(true);
system_logger_1.systemLogger.debug('Development mode enabled - showing all technical details');
}
else if (options.verbose) {
system_logger_1.systemLogger.setDevMode(true);
system_logger_1.systemLogger.debug('Verbose logging enabled');
}
// Disable colors if requested (now handled by loggers)
if (options.noColor) {
// Color handling is now managed by the logger system
}
system_logger_1.systemLogger.info('DebuggAI Test Runner');
if (!system_logger_1.systemLogger.getDevMode()) {
console.log('='.repeat(50));
}
// Get API key
const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY;
if (!apiKey) {
system_logger_1.systemLogger.error('API key is required. Provide it via --api-key or DEBUGGAI_API_KEY environment variable.');
process.exit(1);
}
// Get repository path
const repoPath = options.repoPath ? path.resolve(options.repoPath) : process.cwd();
// Validate repository path exists
if (!await fs.pathExists(repoPath)) {
system_logger_1.systemLogger.error(`Repository path does not exist: ${repoPath}`);
process.exit(1);
}
// Validate it's a git repository
const gitDir = path.join(repoPath, '.git');
if (!await fs.pathExists(gitDir)) {
system_logger_1.systemLogger.error(`Not a git repository: ${repoPath}`);
process.exit(1);
}
system_logger_1.systemLogger.debug('CLI configuration', {
category: 'cli',
details: { repoPath, apiKey: `${apiKey.substring(0, 8)}...` }
});
// Generate a tunnel key for backend to create tunnel endpoints
const tunnelKey = (0, crypto_1.randomUUID)();
system_logger_1.systemLogger.debug('Generated tunnel key', {
category: 'tunnel',
details: { key: tunnelKey.substring(0, 8) + '...' }
});
// Initialize test manager (after all validations pass)
let testManager;
if (options.tunnelUuid) {
// Use new auto-tunnel flow where backend provides ngrok token after commit suite creation
system_logger_1.systemLogger.debug('Using auto-tunnel mode', {
category: 'tunnel',
details: { uuid: options.tunnelUuid }
});
system_logger_1.systemLogger.info(`Using auto-tunnel mode with UUID: ${options.tunnelUuid}`);
testManager = test_manager_1.TestManager.withAutoTunnel({
apiKey,
repoPath,
baseUrl: options.baseUrl,
...(options.outputDir && { testOutputDir: options.outputDir }),
serverTimeout: parseInt(options.serverTimeout) || 60000,
maxTestWaitTime: parseInt(options.maxTestTime) || 600000,
downloadArtifacts: options.downloadArtifacts || false,
tunnelKey, // Pass the generated tunnel key
// Commit analysis options
commit: options.commit,
commitRange: options.commitRange,
since: options.since,
...(options.last && { last: parseInt(options.last) }),
// PR sequence options
prSequence: options.prSequence || false,
baseBranch: options.baseBranch,
headBranch: options.headBranch,
// GitHub App PR testing
...(options.pr && { pr: parseInt(options.pr) })
}, options.tunnelUuid, parseInt(options.tunnelPort) || 3000);
}
else {
// Standard mode with tunnel key for backend tunnel creation
testManager = new test_manager_1.TestManager({
apiKey,
repoPath,
baseUrl: options.baseUrl,
...(options.outputDir && { testOutputDir: options.outputDir }),
serverTimeout: parseInt(options.serverTimeout) || 60000,
maxTestWaitTime: parseInt(options.maxTestTime) || 600000,
downloadArtifacts: options.downloadArtifacts || false,
tunnelKey, // Pass the generated tunnel key
createTunnel: true, // Enable tunnel creation
tunnelPort: parseInt(options.serverPort) || 3000, // Use server port for tunnel
// Commit analysis options
commit: options.commit,
commitRange: options.commitRange,
since: options.since,
...(options.last && { last: parseInt(options.last) }),
// PR sequence options
prSequence: options.prSequence || false,
baseBranch: options.baseBranch,
headBranch: options.headBranch,
// GitHub App PR testing
...(options.pr && { pr: parseInt(options.pr) })
});
}
// Wait for server if requested
if (options.waitForServer) {
const serverPort = parseInt(options.serverPort);
const serverTimeout = parseInt(options.serverTimeout) || 60000;
system_logger_1.systemLogger.debug('Waiting for development server', {
category: 'server',
details: { port: serverPort, timeout: serverTimeout }
});
if (system_logger_1.systemLogger.getDevMode()) {
system_logger_1.systemLogger.debug(`Waiting for development server on port ${serverPort}`, { category: 'server' });
}
else {
system_logger_1.systemLogger.progress.start(`Waiting for development server on port ${serverPort}`);
}
const serverReady = await testManager.waitForServer(serverPort, serverTimeout);
if (!serverReady) {
system_logger_1.systemLogger.error(`Server on port ${serverPort} did not start within ${serverTimeout}ms`);
process.exit(1);
}
}
// Run the tests
if (system_logger_1.systemLogger.getDevMode()) {
system_logger_1.systemLogger.debug('Starting test analysis and generation', { category: 'test' });
}
else {
system_logger_1.systemLogger.progress.start('Starting test analysis and generation');
}
const result = await testManager.runCommitTests();
if (result.success) {
system_logger_1.systemLogger.success('Tests completed successfully!');
if (result.testFiles && result.testFiles.length > 0) {
system_logger_1.systemLogger.displayFileList(result.testFiles, repoPath);
}
system_logger_1.systemLogger.debug('Test suite completed', {
category: 'test',
details: { suiteUuid: result.suiteUuid }
});
system_logger_1.systemLogger.info(`Test suite ID: ${result.suiteUuid}`);
// Check if any tests failed (process.exitCode may have been set by reportResults)
if (process.exitCode === 1) {
system_logger_1.systemLogger.error('Some tests failed - see results above');
process.exit(1);
}
else {
process.exit(0);
}
}
else {
system_logger_1.systemLogger.error(`Tests failed: ${result.error}`);
process.exit(1);
}
}
catch (error) {
// Re-throw test exit errors to prevent them from being handled
if (error instanceof Error && error.isSuccessExit) {
throw error;
}
const errorMsg = error instanceof Error ? error.message : String(error);
system_logger_1.systemLogger.error('Unexpected error: ' + errorMsg);
if (process.env.DEBUG) {
system_logger_1.systemLogger.error('Stack trace: ' + error?.stack);
}
telemetry_1.telemetry.trackCommandComplete('test', false, errorMsg);
await telemetry_1.telemetry.shutdown();
process.exit(1);
}
});
program
.command('status')
.description('Check the status of a test suite')
.requiredOption('-s, --suite-id <id>', 'Test suite UUID')
.option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)')
.option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)')
.option('--dev', 'Enable development logging (shows all technical details)')
.option('--no-color', 'Disable colored output')
.action(async (options) => {
try {
// Track command start
telemetry_1.telemetry.trackCommandStart('status', options);
// Set up development mode
if (options.dev) {
process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG';
process.env.DEBUGGAI_DEV_MODE = 'true';
system_logger_1.systemLogger.debug('Development mode enabled');
}
// Disable colors if requested (now handled by loggers)
if (options.noColor) {
// Color handling is now managed by the logger system
}
system_logger_1.systemLogger.info('DebuggAI Test Status');
console.log('='.repeat(50));
// Get API key
const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY;
if (!apiKey) {
system_logger_1.systemLogger.error('API key is required.');
process.exit(1);
}
// Create a basic test manager just for API access
const testManager = new test_manager_1.TestManager({
apiKey,
repoPath: process.cwd(), // Not used for status check
baseUrl: options.baseUrl
});
// Get test suite status
const suite = await testManager.client.getCommitTestSuiteStatus(options.suiteId);
if (!suite) {
system_logger_1.systemLogger.error(`Test suite not found: ${options.suiteId}`);
process.exit(1);
}
system_logger_1.systemLogger.info(`Suite ID: ${suite.uuid}`);
system_logger_1.systemLogger.info(`Name: ${suite.name || 'Unnamed'}`);
system_logger_1.systemLogger.info(`Status: ${getStatusColor(suite.status || 'unknown')}`);
system_logger_1.systemLogger.info(`Tests: ${suite.tests?.length || 0}`);
if (suite.tests && suite.tests.length > 0) {
system_logger_1.systemLogger.info('\nTest Details:');
for (const test of suite.tests) {
const status = test.curRun?.status || 'unknown';
console.log(` • ${test.name || test.uuid}: ${getStatusColor(status)}`);
}
}
// Track successful completion
telemetry_1.telemetry.trackCommandComplete('status', true);
await telemetry_1.telemetry.shutdown();
}
catch (error) {
// Re-throw test exit errors to prevent them from being handled
if (error instanceof Error && error.isSuccessExit) {
throw error;
}
const errorMsg = error instanceof Error ? error.message : String(error);
system_logger_1.systemLogger.error('Error checking status: ' + errorMsg);
telemetry_1.telemetry.trackCommandComplete('status', false, errorMsg);
await telemetry_1.telemetry.shutdown();
process.exit(1);
}
});
program
.command('list')
.description('List test suites for a repository')
.option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)')
.option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)')
.option('-r, --repo <name>', 'Repository name filter')
.option('-b, --branch <name>', 'Branch name filter')
.option('-l, --limit <number>', 'Limit number of results (default: 20)', '20')
.option('-p, --page <number>', 'Page number (default: 1)', '1')
.option('--dev', 'Enable development logging (shows all technical details)')
.option('--no-color', 'Disable colored output')
.action(async (options) => {
try {
// Track command start
telemetry_1.telemetry.trackCommandStart('list', options);
// Set up development mode
if (options.dev) {
process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG';
process.env.DEBUGGAI_DEV_MODE = 'true';
system_logger_1.systemLogger.debug('Development mode enabled');
}
// Disable colors if requested (now handled by loggers)
if (options.noColor) {
// Color handling is now managed by the logger system
}
system_logger_1.systemLogger.info('DebuggAI Test Suites');
console.log('='.repeat(50));
// Get API key
const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY;
if (!apiKey) {
system_logger_1.systemLogger.error('API key is required.');
process.exit(1);
}
// Create a basic test manager just for API access
const testManager = new test_manager_1.TestManager({
apiKey,
repoPath: process.cwd(), // Not used for listing
baseUrl: options.baseUrl
});
// List test suites
const result = await testManager.client.listTestSuites({
repoName: options.repo,
branchName: options.branch,
limit: parseInt(options.limit),
page: parseInt(options.page)
});
if (result.suites.length === 0) {
system_logger_1.systemLogger.warn('No test suites found.');
return;
}
system_logger_1.systemLogger.info(`Found ${result.total} test suites (showing ${result.suites.length}):`);
console.log('');
for (const suite of result.suites) {
console.log(`${suite.name || suite.uuid}`);
console.log(` Status: ${getStatusColor(suite.status || 'unknown')}`);
console.log(` Tests: ${suite.tests?.length || 0}`);
console.log(` UUID: ${suite.uuid}`);
console.log('');
}
// Track successful completion
telemetry_1.telemetry.trackCommandComplete('list', true);
await telemetry_1.telemetry.shutdown();
}
catch (error) {
// Re-throw test exit errors to prevent them from being handled
if (error instanceof Error && error.isSuccessExit) {
throw error;
}
const errorMsg = error instanceof Error ? error.message : String(error);
system_logger_1.systemLogger.error('Error listing test suites: ' + errorMsg);
telemetry_1.telemetry.trackCommandComplete('list', false, errorMsg);
await telemetry_1.telemetry.shutdown();
process.exit(1);
}
});
program
.command('workflow')
.description('Run complete E2E testing workflow with server management and tunnel setup')
.option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)')
.option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)')
.option('-r, --repo-path <path>', 'Repository path (default: current directory)')
.option('-o, --output-dir <dir>', 'Test output directory (default: tests/debugg-ai)')
.option('-p, --port <port>', 'Server port (default: 3000)', '3000')
.option('-c, --command <cmd>', 'Server start command (default: npm start)', 'npm start')
.option('--server-args <args>', 'Server command arguments (comma-separated)')
.option('--server-cwd <path>', 'Server working directory')
.option('--server-env <env>', 'Server environment variables (KEY=value,KEY2=value2)')
.option('--ngrok-token <token>', 'Ngrok auth token (can also use NGROK_AUTH_TOKEN env var)')
.option('--ngrok-subdomain <subdomain>', 'Custom ngrok subdomain')
.option('--ngrok-domain <domain>', 'Custom ngrok domain')
.option('--base-domain <domain>', 'Base domain for tunnels (default: ngrok.debugg.ai)')
.option('--max-test-time <ms>', 'Maximum test wait time in milliseconds (default: 600000)', '600000')
.option('--server-timeout <ms>', 'Server startup timeout in milliseconds (default: 60000)', '60000')
.option('--cleanup-on-success', 'Cleanup resources after successful completion (default: true)', true)
.option('--cleanup-on-error', 'Cleanup resources after errors (default: true)', true)
.option('--download-artifacts', 'Download test artifacts (scripts, recordings, JSON results) to local filesystem')
.option('--pr-sequence', 'Enable PR commit sequence testing (sends individual test requests for each commit in PR)')
.option('--base-branch <branch>', 'Base branch for PR testing (auto-detected from GitHub env if not provided)')
.option('--head-branch <branch>', 'Head branch for PR testing (auto-detected from GitHub env if not provided)')
.option('--verbose', 'Verbose logging')
.option('--dev', 'Enable development logging (shows all technical details, server logs, tunnel info)')
.option('--no-color', 'Disable colored output')
.action(async (options) => {
try {
// Set up development mode
if (options.dev) {
process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG';
process.env.DEBUGGAI_DEV_MODE = 'true';
system_logger_1.systemLogger.debug('Development mode enabled');
}
// Disable colors if requested (now handled by loggers)
if (options.noColor) {
// Color handling is now managed by the logger system
}
system_logger_1.systemLogger.info('DebuggAI Workflow Runner');
console.log('='.repeat(50));
// Get API key
const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY;
if (!apiKey) {
system_logger_1.systemLogger.error('API key is required. Provide it via --api-key or DEBUGGAI_API_KEY environment variable.');
process.exit(1);
}
// Get repository path
const repoPath = options.repoPath ? path.resolve(options.repoPath) : process.cwd();
// Validate repository path exists
if (!await fs.pathExists(repoPath)) {
system_logger_1.systemLogger.error(`Repository path does not exist: ${repoPath}`);
process.exit(1);
}
// Validate it's a git repository
const gitDir = path.join(repoPath, '.git');
if (!await fs.pathExists(gitDir)) {
system_logger_1.systemLogger.error(`Not a git repository: ${repoPath}`);
process.exit(1);
}
system_logger_1.systemLogger.debug('Workflow configuration', {
category: 'workflow',
details: { repoPath, apiKey: `${apiKey.substring(0, 8)}...` }
});
// Parse server command and args
const [command, ...defaultArgs] = options.command.split(' ');
const serverArgs = options.serverArgs
? options.serverArgs.split(',').map((arg) => arg.trim())
: defaultArgs;
// Parse environment variables
const serverEnv = {};
if (options.serverEnv) {
options.serverEnv.split(',').forEach((pair) => {
const [key, value] = pair.trim().split('=');
if (key && value) {
serverEnv[key] = value;
}
});
}
// Generate a tunnel key for backend to create tunnel endpoints
const tunnelKey = (0, crypto_1.randomUUID)();
system_logger_1.systemLogger.debug('Generated tunnel key for workflow', {
category: 'tunnel',
details: { key: tunnelKey.substring(0, 8) + '...' }
});
// Initialize workflow orchestrator
const orchestrator = new workflow_orchestrator_1.WorkflowOrchestrator({
ngrokAuthToken: options.ngrokToken || process.env.NGROK_AUTH_TOKEN,
baseDomain: options.baseDomain,
verbose: options.verbose || options.dev, // Dev mode implies verbose
devMode: options.dev
});
// Configure workflow
const workflowConfig = {
server: {
command,
args: serverArgs,
port: parseInt(options.port),
cwd: options.serverCwd || repoPath,
env: serverEnv,
startupTimeout: parseInt(options.serverTimeout)
},
tunnel: {
port: parseInt(options.port),
subdomain: options.ngrokSubdomain,
customDomain: options.ngrokDomain,
authtoken: options.ngrokToken || process.env.NGROK_AUTH_TOKEN
},
test: {
apiKey,
baseUrl: options.baseUrl,
repoPath,
testOutputDir: options.outputDir,
maxTestWaitTime: parseInt(options.maxTestTime),
downloadArtifacts: options.downloadArtifacts || false,
tunnelKey, // Add the generated tunnel key
createTunnel: true, // Enable tunnel creation
tunnelPort: parseInt(options.port) || 3000, // Use server port for tunnel
// PR sequence options
prSequence: options.prSequence || false,
baseBranch: options.baseBranch,
headBranch: options.headBranch,
// GitHub App PR testing
...(options.pr && { pr: parseInt(options.pr) })
},
cleanup: {
onSuccess: options.cleanupOnSuccess,
onError: options.cleanupOnError
}
};
system_logger_1.systemLogger.debug('Starting workflow orchestrator');
system_logger_1.systemLogger.progress.start('Starting complete testing workflow');
const result = await orchestrator.executeWorkflow(workflowConfig);
if (result.success) {
system_logger_1.systemLogger.success('Workflow completed successfully!');
if (result.tunnelInfo) {
system_logger_1.systemLogger.info(`Tunnel URL: ${result.tunnelInfo.url}`);
}
if (result.serverUrl) {
system_logger_1.systemLogger.info(`Local Server: ${result.serverUrl}`);
}
if (result.testResult?.testFiles && result.testResult.testFiles.length > 0) {
system_logger_1.systemLogger.displayFileList(result.testResult.testFiles, repoPath);
}
if (result.testResult?.suiteUuid) {
system_logger_1.systemLogger.info(`Test suite ID: ${result.testResult.suiteUuid}`);
}
// Check if any tests failed (process.exitCode may have been set by reportResults)
if (process.exitCode === 1) {
system_logger_1.systemLogger.error('Some tests failed - see results above');
process.exit(1);
}
else {
process.exit(0);
}
}
else {
system_logger_1.systemLogger.error(`Workflow failed: ${result.error}`);
process.exit(1);
}
}
catch (error) {
// Re-throw test exit errors to prevent them from being handled
if (error instanceof Error && error.isSuccessExit) {
throw error;
}
system_logger_1.systemLogger.error('Unexpected workflow error: ' + (error instanceof Error ? error.message : String(error)));
if (process.env.DEBUG) {
system_logger_1.systemLogger.error('Stack trace: ' + error?.stack);
}
process.exit(1);
}
});
/**
* Get colored status text
*/
function getStatusColor(status) {
switch (status) {
case 'completed':
return '✓ COMPLETED';
case 'failed':
return '✗ FAILED';
case 'running':
return '⏳ RUNNING';
case 'pending':
return '⏸ PENDING';
default:
return '❓ UNKNOWN';
}
}
// Handle unhandled promise rejections and uncaught exceptions
// Only add these handlers if we're not in a test environment
if (process.env.NODE_ENV !== 'test') {
process.on('unhandledRejection', (reason, promise) => {
system_logger_1.systemLogger.error('Unhandled Rejection at: promise=' + promise + ', reason=' + reason);
process.exit(1);
});
process.on('uncaughtException', (error) => {
system_logger_1.systemLogger.error('Uncaught Exception: ' + error);
process.exit(1);
});
}
// Parse command line arguments
program.parse();
//# sourceMappingURL=cli.js.map
;