Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
84e9f43
feat(tracker): add tracker init command
eduardoRoth Nov 20, 2025
028bfa5
fix(tracker): update code based on biome ci
eduardoRoth Nov 20, 2025
e7c3b19
fix(tracker): update tests for windows paths
eduardoRoth Nov 20, 2025
08fe76b
Merge branch 'refs/heads/main' into feat/eroth/tracker-run
eduardoRoth Nov 24, 2025
5cd47dd
Merge branch 'refs/heads/main' into feat/eroth/tracker-run
eduardoRoth Nov 24, 2025
bd75b7d
feat(tracker): added run command
eduardoRoth Nov 26, 2025
2766875
chore(tracker): update ignorePatterns default value
eduardoRoth Nov 26, 2025
85cee57
chore(tracker): remove deprecated jsToPairs property
eduardoRoth Nov 26, 2025
00ee03a
chore(tracker): fix biome import order
eduardoRoth Nov 26, 2025
8fb6843
chore(tracker): update readme with tracker run info
eduardoRoth Nov 26, 2025
ef2f3a8
fix(tracker): show warning when no files are found
eduardoRoth Dec 2, 2025
a20b649
chore(): format, lint, biome ci
eduardoRoth Dec 2, 2025
7b88ff9
feat(tracker): use glob package
eduardoRoth Dec 2, 2025
85c4100
chore(tracker): remove empty finally
eduardoRoth Dec 2, 2025
98c9f52
chore(tracker): add note about globSync
eduardoRoth Dec 2, 2025
9cff0d4
chore(tracker): replace global with node cwd
eduardoRoth Dec 2, 2025
10edd76
fix(tracker): update tests to handle Windows backslash
eduardoRoth Dec 2, 2025
9e8fd5c
chore(tracker): formatting
eduardoRoth Dec 2, 2025
13eb3dd
Merge branch 'main' into feat/eroth/tracker-run
eduardoRoth Dec 3, 2025
3234d1c
chore(): update package-lock
eduardoRoth Dec 3, 2025
e33e99b
chore(): apply pr comments
eduardoRoth Dec 3, 2025
d5bb781
fix(tracker): update tests to vitest
eduardoRoth Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ USAGE
* [`hd report committers`](#hd-report-committers)
* [`hd scan eol`](#hd-scan-eol)
* [`hd tracker init`](#hd-tracker-init)
* [`hd tracker run`](#hd-tracker-run)
* [`hd update [CHANNEL]`](#hd-update-channel)
* **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.

Expand Down Expand Up @@ -233,6 +234,31 @@ EXAMPLES

_See code: [src/commands/tracker/init.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/tracker/init.ts)_

## `hd tracker run`

Run the tracker

```
USAGE
$ hd tracker run [-d <value>] [-f <value>]

FLAGS
-d, --configDir=<value> [default: hd-tracker] Directory where the tracker configuration file resides
-f, --configFile=<value> [default: config.json] Filename for the tracker configuration file

DESCRIPTION
Run the tracker

EXAMPLES
$ hd tracker run

$ hd tracker run -d tracker-configuration

$ hd tracker run -d tracker -f settings.json
```

_See code: [src/commands/tracker/run.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/tracker/run.ts)_

## `hd update [CHANNEL]`

update the hd CLI
Expand Down
2,455 changes: 352 additions & 2,103 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,28 @@
"@oclif/plugin-help": "^6.2.32",
"@oclif/plugin-update": "^4.7.14",
"@oclif/table": "^0.5.1",
"cli-progress": "^3.12.0",
"date-fns": "^4.1.0",
"glob": "^13.0.0",
"graphql": "^16.11.0",
"node-machine-id": "^1.1.12",
"ora": "^9.0.0",
"packageurl-js": "^2.0.1",
"sloc": "^0.3.2",
"terminal-link": "^5.0.0",
"update-notifier": "^7.3.1"
},
"devDependencies": {
"@biomejs/biome": "^2.3.4",
"@oclif/test": "^4.1.13",
"@types/cli-progress": "^3.11.6",
"@types/inquirer": "^9.0.9",
"@types/mock-fs": "^4.13.4",
"@types/node": "^24.10.0",
"@types/ora": "^3.1.0",
"@types/sinon": "^17.0.4",
"@types/sloc": "^0.2.3",
"@types/terminal-link": "^1.1.0",
"@types/update-notifier": "^6.0.8",
"globstar": "^1.0.0",
"mock-fs": "^5.5.0",
Expand Down Expand Up @@ -121,4 +128,4 @@
}
},
"types": "dist/index.d.ts"
}
}
221 changes: 221 additions & 0 deletions src/commands/tracker/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { spawnSync } from 'node:child_process';
import { cwd } from 'node:process';
import { Command, Flags, ux } from '@oclif/core';
import { Presets, SingleBar } from 'cli-progress';
import ora from 'ora';
import terminalLink from 'terminal-link';
import { TRACKER_GIT_OUTPUT_FORMAT } from '../../config/constants.js';
import { getErrorMessage, isErrnoException } from '../../service/error.svc.js';
import {
type CategoryStatsResult,
type FilesStats,
type GitLastCommit,
getConfiguration,
getFileStats,
getFilesFromCategory,
getRootDir,
INITIAL_FILES_STATS,
saveResults,
} from '../../service/tracker.svc.js';

export default class Run extends Command {
static override description = 'Run the tracker';
static enableJsonFlag = false;
static override examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> -d tracker-configuration',
'<%= config.bin %> <%= command.id %> -d tracker -f settings.json',
];

static override flags = {
configDir: Flags.string({
char: 'd',
description: 'Directory where the tracker configuration file resides',
default: 'hd-tracker',
}),
configFile: Flags.string({
char: 'f',
description: 'Filename for the tracker configuration file',
default: 'config.json',
}),
};

public async run(): Promise<void> {
const { flags } = await this.parse(Run);
const { configDir, configFile } = flags;

try {
const rootDir = getRootDir(cwd());
const confSpinner = ora('Searching for configuration file').start();
const { categories, ignorePatterns, outputDir } = getConfiguration(rootDir, configDir, configFile);

confSpinner.text = `Configuration file ${configFile} found in ${rootDir}/${configDir}`;

const categoriesTotal = Object.keys(categories).length;
if (categoriesTotal > 0) {
confSpinner.stopAndPersist({
text: ux.colorize('green', `Found ${categoriesTotal} categor${categoriesTotal === 1 ? 'y' : 'ies'}`),
symbol: ux.colorize('green', `\u2714`),
});
} else {
confSpinner.stopAndPersist({
text: ux.colorize('red', `No categories found, please check your configuration file`),
symbol: ux.colorize('red', `\u2716`),
});
return;
}
this.log('');
const results = Object.entries(categories).reduce((acc: CategoryStatsResult[], [name, category]) => {
const loadingFilesSpinner = ora(`[${ux.colorize('blueBright', name)}] Getting files`).start();
const fileProgress = new SingleBar(
{
format: `${ux.colorize('green', '{bar}')} | {value}/{total} | {name}`,
clearOnComplete: false,
fps: 100,
hideCursor: true,
},
Presets.shades_grey,
);

const fileTypes: Set<string> = new Set();
const categoryFilesWithError: string[] = [];

const files = getFilesFromCategory(category, {
rootDir,
ignorePatterns,
});

if (files.length === 0) {
loadingFilesSpinner.stopAndPersist({
text: ux.colorize('yellow', `[${ux.colorize('yellowBright', name)}] Found 0 files`),
symbol: ux.colorize('yellowBright', `\u26A0`),
});
this.log(
ux.colorize(
'yellow',
`Please check your configuration [includes] property so it matches folders in your project directory`,
),
);
this.log('');
return acc;
}

loadingFilesSpinner.stopAndPersist({
text: ux.colorize('green', `[${ux.colorize('blueBright', name)}] Found ${files.length} files`),
symbol: ux.colorize('green', `\u2714`),
});
fileProgress.start(files.length, 1);

const fileResults = files.reduce((result: FilesStats, file, currentIndex, array) => {
const fileStats = getFileStats(file, {
rootDir,
});
if (currentIndex === array.length - 1) {
fileProgress.update({
name: ux.colorize('green', 'All files were processed successfully'),
});
fileProgress.stop();
} else {
fileProgress.increment({
name: file,
});
}

if ('error' in fileStats) {
categoryFilesWithError.push(file);
fileProgress.increment();
return result;
} else {
fileTypes.add(fileStats.fileType);
return {
total: fileStats.total + result.total,
block: fileStats.block + result.block,
blockEmpty: fileStats.blockEmpty + result.blockEmpty,
comment: fileStats.comment + result.comment,
empty: fileStats.empty + result.empty,
mixed: fileStats.mixed + result.mixed,
single: fileStats.single + result.single,
source: fileStats.source + result.source,
todo: fileStats.todo + result.todo,
};
}
}, INITIAL_FILES_STATS);

this.log('');
acc.push({
name,
totals: fileResults,
errors: categoryFilesWithError,
fileTypes: Array.from(fileTypes),
});
return acc;
}, []);

this.log('');
const spinner = ora('Saving results').start();
const resultsLink = saveResults(results, rootDir, outputDir, this.fetchGitLastCommit(rootDir));
spinner.stopAndPersist({
text: ux.colorize('green', 'Tracker results saved!\n'),
symbol: ux.colorize('green', '\u2713'),
});

this.log(`${ux.colorize('blueBright', terminalLink(`Open Tracker Results`, `file://${resultsLink}`))}\n`);
} catch (err) {
if (err instanceof Error) {
this.error(ux.colorize('red', err.message));
}
}
}

/**
* Fetches Git last commit
*/
private fetchGitLastCommit(rootDir?: string): GitLastCommit {
const logParameters = ['log', `-1`, `--format=${TRACKER_GIT_OUTPUT_FORMAT}`, ...(rootDir ? ['--', rootDir] : [])];

const logProcess = spawnSync('git', logParameters, {
encoding: 'utf-8',
});

if (logProcess.error) {
if (isErrnoException(logProcess.error)) {
if (logProcess.error.code === 'ENOENT') {
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
}
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
}
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
}

if (logProcess.status !== 0) {
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
}

if (!logProcess.stdout) {
return {
hash: '',
timestamp: '',
author: '',
};
}

return logProcess.stdout
.split('\n')
.filter(Boolean)
.reduce(
(_acc, curr) => {
const [hash, author, timestamp] = curr.replace(/^"(.*)"$/, '$1').split('|');
return {
timestamp,
hash,
author,
};
},
{
hash: '',
timestamp: '',
author: '',
},
);
}
}
4 changes: 4 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const toBoolean = (value: string | undefined): boolean | undefined => {
return undefined;
};

// Trackers - Constants
export const DEFAULT_TRACKER_RUN_DATA_FILE = 'data.json';
export const TRACKER_GIT_OUTPUT_FORMAT = `"${['%H', '%an', '%ad'].join('|')}"`;

let concurrentPageRequests = CONCURRENT_PAGE_REQUESTS;
const parsed = Number.parseInt(process.env.CONCURRENT_PAGE_REQUESTS ?? '0', 10);
if (parsed > 0) {
Expand Down
4 changes: 1 addition & 3 deletions src/config/tracker.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ export const TRACKER_DEFAULT_CONFIG: TrackerConfig = {
legacy: {
fileTypes: ['js', 'ts', 'html', 'css', 'scss', 'less'],
includes: ['./legacy'],
jsTsPairs: 'js',
},
modern: {
fileTypes: ['ts', 'html', 'css', 'scss', 'less'],
includes: ['./modern'],
jsTsPairs: 'ts',
},
},
ignorePatterns: ['node_modules'],
ignorePatterns: ['**/node_modules/**'],
outputDir: 'hd-tracker',
configFile: 'config.json',
};
Loading