import { TsTest } from './tstest.classes.tstest.js'; import type { LogOptions } from './tstest.logging.js'; export enum TestExecutionMode { DIRECTORY = 'directory', FILE = 'file', GLOB = 'glob' } export const runCli = async () => { // Check if we're using global tstest in the tstest project itself try { const packageJsonPath = `${process.cwd()}/package.json`; const fs = await import('fs'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJson.name === '@git.zone/tstest') { // Check if we're running from a global installation const execPath = process.argv[1]; // Debug: log the paths (uncomment for debugging) // console.log('DEBUG: Checking global tstest usage...'); // console.log('execPath:', execPath); // console.log('cwd:', process.cwd()); // console.log('process.argv:', process.argv); // Check if this is running from global installation const isLocalCli = execPath.includes(process.cwd()); const isGlobalPnpm = process.argv.some(arg => arg.includes('.pnpm') && !arg.includes(process.cwd())); const isGlobalNpm = process.argv.some(arg => arg.includes('npm/node_modules') && !arg.includes(process.cwd())); if (!isLocalCli && (isGlobalPnpm || isGlobalNpm || !execPath.includes('node_modules'))) { console.error('\n⚠️ WARNING: You are using a globally installed tstest in the tstest project itself!'); console.error(' This means you are NOT testing your local changes.'); console.error(' Please use one of these commands instead:'); console.error(' • node cli.js '); console.error(' • pnpm test '); console.error(' • ./cli.js (if executable)\n'); } } } } catch (error) { // Silently ignore any errors in this check } // Parse command line arguments const args = process.argv.slice(2); const logOptions: LogOptions = {}; let testPath: string | null = null; let tags: string[] = []; let startFromFile: number | null = null; let stopAtFile: number | null = null; let timeoutSeconds: number | null = null; let watchMode: boolean = false; let watchIgnorePatterns: string[] = []; // Parse options for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--version': // Get version from package.json try { const fs = await import('fs'); const packagePath = new URL('../package.json', import.meta.url).pathname; const packageData = JSON.parse(await fs.promises.readFile(packagePath, 'utf8')); console.log(`tstest version ${packageData.version}`); } catch (error) { console.log('tstest version unknown'); } process.exit(0); break; case '--quiet': case '-q': logOptions.quiet = true; break; case '--verbose': case '-v': logOptions.verbose = true; break; case '--no-color': logOptions.noColor = true; break; case '--json': logOptions.json = true; break; case '--log-file': case '--logfile': logOptions.logFile = true; // Set this as a flag, not a value break; case '--tags': if (i + 1 < args.length) { tags = args[++i].split(','); } break; case '--startFrom': if (i + 1 < args.length) { const value = parseInt(args[++i], 10); if (isNaN(value) || value < 1) { console.error('Error: --startFrom must be a positive integer'); process.exit(1); } startFromFile = value; } else { console.error('Error: --startFrom requires a number argument'); process.exit(1); } break; case '--stopAt': if (i + 1 < args.length) { const value = parseInt(args[++i], 10); if (isNaN(value) || value < 1) { console.error('Error: --stopAt must be a positive integer'); process.exit(1); } stopAtFile = value; } else { console.error('Error: --stopAt requires a number argument'); process.exit(1); } break; case '--timeout': if (i + 1 < args.length) { const value = parseInt(args[++i], 10); if (isNaN(value) || value < 1) { console.error('Error: --timeout must be a positive integer (seconds)'); process.exit(1); } timeoutSeconds = value; } else { console.error('Error: --timeout requires a number argument (seconds)'); process.exit(1); } break; case '--watch': case '-w': watchMode = true; break; case '--watch-ignore': if (i + 1 < args.length) { watchIgnorePatterns = args[++i].split(','); } else { console.error('Error: --watch-ignore requires a comma-separated list of patterns'); process.exit(1); } break; default: if (!arg.startsWith('-')) { testPath = arg; } } } // Validate test file range options if (startFromFile !== null && stopAtFile !== null && startFromFile > stopAtFile) { console.error('Error: --startFrom cannot be greater than --stopAt'); process.exit(1); } if (!testPath) { console.error('You must specify a test directory/file/pattern as argument. Please try again.'); console.error('\nUsage: tstest [options]'); console.error('\nOptions:'); console.error(' --version Show version information'); console.error(' --quiet, -q Minimal output'); console.error(' --verbose, -v Verbose output'); console.error(' --no-color Disable colored output'); console.error(' --json Output results as JSON'); console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log'); console.error(' --tags Run only tests with specified tags (comma-separated)'); console.error(' --startFrom Start running from test file number n'); console.error(' --stopAt Stop running at test file number n'); console.error(' --timeout Timeout test files after s seconds'); console.error(' --watch, -w Watch for file changes and re-run tests'); console.error(' --watch-ignore Patterns to ignore in watch mode (comma-separated)'); process.exit(1); } let executionMode: TestExecutionMode; // Detect execution mode based on the argument if (testPath.includes('*') || testPath.includes('?') || testPath.includes('[') || testPath.includes('{')) { executionMode = TestExecutionMode.GLOB; } else if (testPath.endsWith('.ts')) { executionMode = TestExecutionMode.FILE; } else { executionMode = TestExecutionMode.DIRECTORY; } const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile, timeoutSeconds); if (watchMode) { await tsTestInstance.runWatch(watchIgnorePatterns); } else { await tsTestInstance.run(); } }; // Execute CLI when this file is run directly if (import.meta.url === `file://${process.argv[1]}`) { runCli(); }