feat: implement line counter as node console app in typescript
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# The MIT License (MIT)
|
||||
C
|
||||
opyright (c) 2025 Stevan Freeborn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Line Counter
|
||||
|
||||
This is a console app that monitors a git repository and reports on the number of lines changed in the repository while it is running. The results are displayed in the console and are written to a file.
|
||||
|
||||
## Installation
|
||||
|
||||
```pwsh
|
||||
npm install -g @stevanfreeborn/line-counter
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```pwsh
|
||||
line-counter <path-to-repo> <path-to-output-dir> [polling-interval-in-seconds]
|
||||
```
|
||||
|
||||
Generated
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@stevanfreeborn/line-counter",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@stevanfreeborn/line-counter",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"line-counter": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@stevanfreeborn/line-counter",
|
||||
"version": "0.0.1",
|
||||
"description": "A simple line counter for reporting the number of lines changed in committed and uncommitted files.",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"line-counter": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"line",
|
||||
"counter",
|
||||
"git",
|
||||
"commit",
|
||||
"uncommitted"
|
||||
],
|
||||
"author": "Stevan Freeborn",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE.md"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import readline from "readline";
|
||||
import fs from "fs";
|
||||
import { changeToRepoDirectory, ensureGitInstalled, getCommitDiff, parseArgs, wait } from "./utils.js";
|
||||
|
||||
ensureGitInstalled();
|
||||
|
||||
const args = parseArgs(process);
|
||||
|
||||
let exitRequested = false;
|
||||
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
process.stdin.on("keypress", (_, key) => {
|
||||
if (key.name === "c" && key.ctrl) {
|
||||
console.log("Ctrl+C pressed. Exiting and restoring original directory...");
|
||||
process.chdir(args.originalLocation);
|
||||
exitRequested = true;
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
changeToRepoDirectory(args.repoPath);
|
||||
|
||||
const startCommit = execSync("git rev-parse HEAD", {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
try {
|
||||
while (exitRequested === false) {
|
||||
const committedDiff = getCommitDiff(startCommit);
|
||||
const uncommittedDiff = getCommitDiff();
|
||||
|
||||
const totalAdded = committedDiff.added + uncommittedDiff.added;
|
||||
const totalRemoved = committedDiff.removed + uncommittedDiff.removed;
|
||||
const totalChanged = totalAdded + totalRemoved;
|
||||
|
||||
const output = `Total lines changed: ${totalChanged} (Committed: +${committedDiff.added}/-${committedDiff.removed}, Uncommitted: +${uncommittedDiff.added}/-${uncommittedDiff.removed})`;
|
||||
|
||||
console.clear();
|
||||
console.log(output);
|
||||
|
||||
fs.writeFileSync(args.outputPath, output, { encoding: "utf-8" });
|
||||
|
||||
await wait(args.pollIntervalSeconds);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("An error occurred: ", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
process.chdir(args.originalLocation);
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
|
||||
export function parseArgs(process: NodeJS.Process) {
|
||||
const repoPath = process.argv[2];
|
||||
const outputPath = process.argv[3];
|
||||
const pollInterval = process.argv[4];
|
||||
|
||||
const usageMessage = `
|
||||
Usage: line-counter <path-to-repo> <output-path> [poll-interval]
|
||||
|
||||
Arguments:
|
||||
path-to-repo Path to the repository to monitor.
|
||||
output-path Path to the output directory where the results will be saved.
|
||||
poll-interval Interval in seconds to poll for changes (default: 10s).
|
||||
`;
|
||||
|
||||
if (process.argv[2] === "--help") {
|
||||
console.log(usageMessage);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (repoPath === undefined) {
|
||||
console.error("Please provide a path to the repository.");
|
||||
console.error(usageMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const repoPathExists = fs.existsSync(repoPath);
|
||||
|
||||
if (!repoPathExists) {
|
||||
console.error(`The specified repository path does not exist: ${repoPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (outputPath === undefined) {
|
||||
console.error("Please provide a path to the output directory.");
|
||||
console.error(usageMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputPathExists = fs.existsSync(outputPath);
|
||||
|
||||
if (!outputPathExists) {
|
||||
console.error(`The specified output path does not exist: ${outputPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputPathIsDirectory = fs.lstatSync(outputPath).isDirectory();
|
||||
|
||||
if (outputPathIsDirectory === false) {
|
||||
console.error(
|
||||
`The specified output path is not a directory: ${outputPath}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pollIntervalString = pollInterval || "10";
|
||||
const pollIntervalSeconds = parseInt(pollIntervalString, 10);
|
||||
const originalLocation = process.cwd();
|
||||
|
||||
return {
|
||||
repoPath,
|
||||
outputPath: `${outputPath}/line-counter-output.txt`,
|
||||
pollIntervalSeconds,
|
||||
originalLocation,
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureGitInstalled() {
|
||||
try {
|
||||
const gitVersion = execSync("git --version", { encoding: "utf-8" });
|
||||
const gitVersionRegex = /git version .+/;
|
||||
|
||||
if (!gitVersionRegex.test(gitVersion)) {
|
||||
console.error(
|
||||
"Git is not installed or not found in PATH. Please install Git and try again.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to detect if Git is installed.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function changeToRepoDirectory(repoPath: string) {
|
||||
try {
|
||||
process.chdir(repoPath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to change directory to ${repoPath}: `, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCommitDiff(startCommit?: string) {
|
||||
try {
|
||||
const command = startCommit
|
||||
? `git diff --shortstat ${startCommit} HEAD`
|
||||
: "git diff --shortstat";
|
||||
|
||||
const diff = execSync(command, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
const addMatch = diff.match(/(\d+) insertion/);
|
||||
const removeMatch = diff.match(/(\d+) deletion/);
|
||||
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
|
||||
if (addMatch && addMatch[1]) {
|
||||
added = parseInt(addMatch[1], 10);
|
||||
}
|
||||
|
||||
if (removeMatch && removeMatch[1]) {
|
||||
removed = parseInt(removeMatch[1], 10);
|
||||
}
|
||||
|
||||
return { added, removed };
|
||||
} catch (error) {
|
||||
console.error("Error executing git diff: ", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function wait(seconds: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2022",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"module": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"lib": ["es2022"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user