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