|
| 1 | +import { spawnSync } from 'node:child_process'; |
| 2 | +import { cwd } from 'node:process'; |
| 3 | +import { Command, Flags, ux } from '@oclif/core'; |
| 4 | +import { Presets, SingleBar } from 'cli-progress'; |
| 5 | +import ora from 'ora'; |
| 6 | +import terminalLink from 'terminal-link'; |
| 7 | +import { TRACKER_GIT_OUTPUT_FORMAT } from '../../config/constants.js'; |
| 8 | +import { getErrorMessage, isErrnoException } from '../../service/error.svc.js'; |
| 9 | +import { |
| 10 | + type CategoryStatsResult, |
| 11 | + type FilesStats, |
| 12 | + type GitLastCommit, |
| 13 | + getConfiguration, |
| 14 | + getFileStats, |
| 15 | + getFilesFromCategory, |
| 16 | + getRootDir, |
| 17 | + INITIAL_FILES_STATS, |
| 18 | + saveResults, |
| 19 | +} from '../../service/tracker.svc.js'; |
| 20 | + |
| 21 | +export default class Run extends Command { |
| 22 | + static override description = 'Run the tracker'; |
| 23 | + static enableJsonFlag = false; |
| 24 | + static override examples = [ |
| 25 | + '<%= config.bin %> <%= command.id %>', |
| 26 | + '<%= config.bin %> <%= command.id %> -d tracker-configuration', |
| 27 | + '<%= config.bin %> <%= command.id %> -d tracker -f settings.json', |
| 28 | + ]; |
| 29 | + |
| 30 | + static override flags = { |
| 31 | + configDir: Flags.string({ |
| 32 | + char: 'd', |
| 33 | + description: 'Directory where the tracker configuration file resides', |
| 34 | + default: 'hd-tracker', |
| 35 | + }), |
| 36 | + configFile: Flags.string({ |
| 37 | + char: 'f', |
| 38 | + description: 'Filename for the tracker configuration file', |
| 39 | + default: 'config.json', |
| 40 | + }), |
| 41 | + }; |
| 42 | + |
| 43 | + public async run(): Promise<void> { |
| 44 | + const { flags } = await this.parse(Run); |
| 45 | + const { configDir, configFile } = flags; |
| 46 | + |
| 47 | + try { |
| 48 | + const rootDir = getRootDir(cwd()); |
| 49 | + const confSpinner = ora('Searching for configuration file').start(); |
| 50 | + const { categories, ignorePatterns, outputDir } = getConfiguration(rootDir, configDir, configFile); |
| 51 | + |
| 52 | + confSpinner.text = `Configuration file ${configFile} found in ${rootDir}/${configDir}`; |
| 53 | + |
| 54 | + const categoriesTotal = Object.keys(categories).length; |
| 55 | + if (categoriesTotal > 0) { |
| 56 | + confSpinner.stopAndPersist({ |
| 57 | + text: ux.colorize('green', `Found ${categoriesTotal} categor${categoriesTotal === 1 ? 'y' : 'ies'}`), |
| 58 | + symbol: ux.colorize('green', `\u2714`), |
| 59 | + }); |
| 60 | + } else { |
| 61 | + confSpinner.stopAndPersist({ |
| 62 | + text: ux.colorize('red', `No categories found, please check your configuration file`), |
| 63 | + symbol: ux.colorize('red', `\u2716`), |
| 64 | + }); |
| 65 | + return; |
| 66 | + } |
| 67 | + this.log(''); |
| 68 | + const results = Object.entries(categories).reduce((acc: CategoryStatsResult[], [name, category]) => { |
| 69 | + const loadingFilesSpinner = ora(`[${ux.colorize('blueBright', name)}] Getting files`).start(); |
| 70 | + const fileProgress = new SingleBar( |
| 71 | + { |
| 72 | + format: `${ux.colorize('green', '{bar}')} | {value}/{total} | {name}`, |
| 73 | + clearOnComplete: false, |
| 74 | + fps: 100, |
| 75 | + hideCursor: true, |
| 76 | + }, |
| 77 | + Presets.shades_grey, |
| 78 | + ); |
| 79 | + |
| 80 | + const fileTypes: Set<string> = new Set(); |
| 81 | + const categoryFilesWithError: string[] = []; |
| 82 | + |
| 83 | + const files = getFilesFromCategory(category, { |
| 84 | + rootDir, |
| 85 | + ignorePatterns, |
| 86 | + }); |
| 87 | + |
| 88 | + if (files.length === 0) { |
| 89 | + loadingFilesSpinner.stopAndPersist({ |
| 90 | + text: ux.colorize('yellow', `[${ux.colorize('yellowBright', name)}] Found 0 files`), |
| 91 | + symbol: ux.colorize('yellowBright', `\u26A0`), |
| 92 | + }); |
| 93 | + this.log( |
| 94 | + ux.colorize( |
| 95 | + 'yellow', |
| 96 | + `Please check your configuration [includes] property so it matches folders in your project directory`, |
| 97 | + ), |
| 98 | + ); |
| 99 | + this.log(''); |
| 100 | + return acc; |
| 101 | + } |
| 102 | + |
| 103 | + loadingFilesSpinner.stopAndPersist({ |
| 104 | + text: ux.colorize('green', `[${ux.colorize('blueBright', name)}] Found ${files.length} files`), |
| 105 | + symbol: ux.colorize('green', `\u2714`), |
| 106 | + }); |
| 107 | + fileProgress.start(files.length, 1); |
| 108 | + |
| 109 | + const fileResults = files.reduce((result: FilesStats, file, currentIndex, array) => { |
| 110 | + const fileStats = getFileStats(file, { |
| 111 | + rootDir, |
| 112 | + }); |
| 113 | + if (currentIndex === array.length - 1) { |
| 114 | + fileProgress.update({ |
| 115 | + name: ux.colorize('green', 'All files were processed successfully'), |
| 116 | + }); |
| 117 | + fileProgress.stop(); |
| 118 | + } else { |
| 119 | + fileProgress.increment({ |
| 120 | + name: file, |
| 121 | + }); |
| 122 | + } |
| 123 | + |
| 124 | + if ('error' in fileStats) { |
| 125 | + categoryFilesWithError.push(file); |
| 126 | + fileProgress.increment(); |
| 127 | + return result; |
| 128 | + } else { |
| 129 | + fileTypes.add(fileStats.fileType); |
| 130 | + return { |
| 131 | + total: fileStats.total + result.total, |
| 132 | + block: fileStats.block + result.block, |
| 133 | + blockEmpty: fileStats.blockEmpty + result.blockEmpty, |
| 134 | + comment: fileStats.comment + result.comment, |
| 135 | + empty: fileStats.empty + result.empty, |
| 136 | + mixed: fileStats.mixed + result.mixed, |
| 137 | + single: fileStats.single + result.single, |
| 138 | + source: fileStats.source + result.source, |
| 139 | + todo: fileStats.todo + result.todo, |
| 140 | + }; |
| 141 | + } |
| 142 | + }, INITIAL_FILES_STATS); |
| 143 | + |
| 144 | + this.log(''); |
| 145 | + acc.push({ |
| 146 | + name, |
| 147 | + totals: fileResults, |
| 148 | + errors: categoryFilesWithError, |
| 149 | + fileTypes: Array.from(fileTypes), |
| 150 | + }); |
| 151 | + return acc; |
| 152 | + }, []); |
| 153 | + |
| 154 | + this.log(''); |
| 155 | + const spinner = ora('Saving results').start(); |
| 156 | + const resultsLink = saveResults(results, rootDir, outputDir, this.fetchGitLastCommit(rootDir)); |
| 157 | + spinner.stopAndPersist({ |
| 158 | + text: ux.colorize('green', 'Tracker results saved!\n'), |
| 159 | + symbol: ux.colorize('green', '\u2713'), |
| 160 | + }); |
| 161 | + |
| 162 | + this.log(`${ux.colorize('blueBright', terminalLink(`Open Tracker Results`, `file://${resultsLink}`))}\n`); |
| 163 | + } catch (err) { |
| 164 | + if (err instanceof Error) { |
| 165 | + this.error(ux.colorize('red', err.message)); |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Fetches Git last commit |
| 172 | + */ |
| 173 | + private fetchGitLastCommit(rootDir?: string): GitLastCommit { |
| 174 | + const logParameters = ['log', `-1`, `--format=${TRACKER_GIT_OUTPUT_FORMAT}`, ...(rootDir ? ['--', rootDir] : [])]; |
| 175 | + |
| 176 | + const logProcess = spawnSync('git', logParameters, { |
| 177 | + encoding: 'utf-8', |
| 178 | + }); |
| 179 | + |
| 180 | + if (logProcess.error) { |
| 181 | + if (isErrnoException(logProcess.error)) { |
| 182 | + if (logProcess.error.code === 'ENOENT') { |
| 183 | + this.error('Git command not found. Please ensure git is installed and available in your PATH.'); |
| 184 | + } |
| 185 | + this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`); |
| 186 | + } |
| 187 | + this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`); |
| 188 | + } |
| 189 | + |
| 190 | + if (logProcess.status !== 0) { |
| 191 | + this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`); |
| 192 | + } |
| 193 | + |
| 194 | + if (!logProcess.stdout) { |
| 195 | + return { |
| 196 | + hash: '', |
| 197 | + timestamp: '', |
| 198 | + author: '', |
| 199 | + }; |
| 200 | + } |
| 201 | + |
| 202 | + return logProcess.stdout |
| 203 | + .split('\n') |
| 204 | + .filter(Boolean) |
| 205 | + .reduce( |
| 206 | + (_acc, curr) => { |
| 207 | + const [hash, author, timestamp] = curr.replace(/^"(.*)"$/, '$1').split('|'); |
| 208 | + return { |
| 209 | + timestamp, |
| 210 | + hash, |
| 211 | + author, |
| 212 | + }; |
| 213 | + }, |
| 214 | + { |
| 215 | + hash: '', |
| 216 | + timestamp: '', |
| 217 | + author: '', |
| 218 | + }, |
| 219 | + ); |
| 220 | + } |
| 221 | +} |
0 commit comments