gstatx 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,205 @@
1
1
  # gstatx
2
2
 
3
- To install dependencies:
3
+ 📊 Export statistics from git repositories
4
+
5
+ A CLI tool to analyze and export statistics from one or multiple git repositories.
6
+
7
+ ## Installation
4
8
 
5
9
  ```bash
6
- bun install
10
+ npm install -g gstatx
7
11
  ```
8
12
 
9
- To run:
13
+ Or using npm:
10
14
 
11
15
  ```bash
12
- bun run src/index.ts
16
+ npm install -g gstatx
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # List contributors for a repository
23
+ gstatx contributors ./my-repo
24
+
25
+ # List contributors for multiple repositories
26
+ gstatx contributors ./repo1 ./repo2 ./repo3
27
+
28
+ # Hide commit counts
29
+ gstatx contributors --no-commits ./my-repo
13
30
  ```
14
31
 
15
- This project was created using `bun init` in bun v1.2.14. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
32
+ ## User Guide
33
+
34
+ ### Commands
35
+
36
+ #### `contributors`
37
+
38
+ List contributors for specified git repositories.
39
+
40
+ **Usage:**
41
+ ```bash
42
+ gstatx contributors [options] <repo-path> [repo-path2 ...]
43
+ ```
44
+
45
+ **Options:**
46
+ - `--no-commits` - Hide commit counts in contributor list
47
+ - `--help, -h` - Show help message
48
+
49
+ **Examples:**
50
+ ```bash
51
+ # List contributors with commit counts
52
+ gstatx contributors ./my-repo
53
+
54
+ # List contributors without commit counts
55
+ gstatx contributors --no-commits ./my-repo
56
+
57
+ # List contributors for multiple repositories
58
+ gstatx contributors ./repo1 ./repo2 ./repo3
59
+ ```
60
+
61
+ ### Configuration File
62
+
63
+ You can create a `.gstatxrc.json` file to set default options and repository paths. The config file is automatically searched in the current directory and parent directories.
64
+
65
+ **Config File Location:**
66
+ - Default: `.gstatxrc.json` (searched from current directory up to root)
67
+ - Custom: Use `--config` or `-c` flag to specify a custom path
68
+
69
+ **Configuration Options:**
70
+
71
+ ```json
72
+ {
73
+ "contributors": {
74
+ "no-commits": false
75
+ },
76
+ "cloneIfNotExists": false,
77
+ "repositories": [
78
+ {
79
+ "path": "../my-repo",
80
+ "name": "My Repository",
81
+ "url": "git@github.com:user/repo.git"
82
+ }
83
+ ]
84
+ }
85
+ ```
86
+
87
+ **Configuration Fields:**
88
+
89
+ - `contributors.no-commits` (boolean, optional): Hide commit counts by default
90
+ - `cloneIfNotExists` (boolean, optional): Automatically clone repositories that don't exist locally
91
+ - `repositories` (array, optional): List of repositories to process
92
+ - `path` (string, required): Repository path (relative to config file location)
93
+ - `name` (string, optional): Display name for the repository
94
+ - `url` (string, optional): Git URL for cloning (required if `cloneIfNotExists: true`)
95
+
96
+ **Important Notes:**
97
+ - Repository paths in the config file are resolved relative to the `.gstatxrc.json` file location, not the current working directory
98
+ - If `cloneIfNotExists: true` and a repository doesn't exist, it will be cloned from the `url`
99
+ - CLI arguments always override config file values
100
+
101
+ **Example Config File:**
102
+
103
+ ```json
104
+ {
105
+ "contributors": {
106
+ "no-commits": false
107
+ },
108
+ "cloneIfNotExists": true,
109
+ "repositories": [
110
+ {
111
+ "path": "../project-a",
112
+ "name": "Project A",
113
+ "url": "git@github.com:user/project-a.git"
114
+ },
115
+ {
116
+ "path": "../project-b",
117
+ "name": "Project B",
118
+ "url": "git@github.com:user/project-b.git"
119
+ }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ ### Global Options
125
+
126
+ - `--help, -h` - Show help message
127
+ - `--config, -c <path>` - Specify custom config file path
128
+
129
+ **Examples:**
130
+ ```bash
131
+ # Use custom config file
132
+ gstatx --config ./custom-config.json contributors
133
+
134
+ # Short form
135
+ gstatx -c ./custom-config.json contributors
136
+ ```
137
+
138
+ ### Using Config File Repositories
139
+
140
+ If you have repositories defined in your config file, you can run commands without specifying paths:
141
+
142
+ ```bash
143
+ # Uses repositories from .gstatxrc.json
144
+ gstatx contributors
145
+ ```
146
+
147
+ The tool will automatically use the repositories listed in your config file.
148
+
149
+ ### Auto-Cloning Repositories
150
+
151
+ When `cloneIfNotExists: true` is set in your config:
152
+
153
+ 1. The tool checks if each repository exists at the specified path
154
+ 2. If the repository doesn't exist and a `url` is provided, it will be cloned automatically
155
+ 3. If the repository already exists, it will be used as-is
156
+ 4. If the path exists but isn't a git repository, an error is shown
157
+
158
+ **Example:**
159
+ ```json
160
+ {
161
+ "cloneIfNotExists": true,
162
+ "repositories": [
163
+ {
164
+ "path": "../new-repo",
165
+ "name": "New Repository",
166
+ "url": "git@github.com:user/repo.git"
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ Running `gstatx contributors` will automatically clone the repository if it doesn't exist.
173
+
174
+ ## Development
175
+
176
+ ### Prerequisites
177
+
178
+ - Node.js >= 18.0.0
179
+ - Bun (for development)
180
+
181
+ ### Setup
182
+
183
+ ```bash
184
+ # Install dependencies
185
+ bun install
186
+
187
+ # Build the project
188
+ bun run build
189
+
190
+ # Run locally
191
+ bun run src/index.ts contributors ./my-repo
192
+ ```
193
+
194
+ ### Scripts
195
+
196
+ - `bun run build` - Build the project
197
+ - `bun run start` - Run the CLI locally
198
+ - `bun run check` - Check code quality
199
+ - `bun run check:fix` - Fix code quality issues
200
+ - `bun run lint` - Lint the code
201
+ - `bun run format` - Format the code
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,8 @@
1
+ export interface RepoContributor {
2
+ name: string;
3
+ email: string;
4
+ commits: number;
5
+ }
6
+ export declare function getContributors(repoPath: string): Promise<RepoContributor[]>;
7
+ export declare function showHelp(): void;
8
+ export declare function listContributors(repoPaths: string[], showCommits?: boolean): Promise<void>;
package/dist/index.js CHANGED
@@ -1,2 +1,14 @@
1
1
  #!/usr/bin/env node
2
- async function c(){let b=process.argv.slice(2);if(b.length===0)console.log("Usage: gstatx <repo-path> [repo-path2 ...]"),console.log(" Export statistics for one or more git repositories"),process.exit(1);console.log("Repositories to analyze:",b)}c().catch(console.error);
2
+ import{exec as S}from"node:child_process";import{promisify as D}from"node:util";import{access as F}from"node:fs/promises";import{join as N}from"node:path";async function Z(q){try{return await F(N(q,".git")),!0}catch{return!1}}var E=D(S);async function v(q){try{let{stdout:Q}=await E("git shortlog -sn --all",{cwd:q}),z=[],K=Q.trim().split(`
3
+ `);for(let B of K){let J=B.match(/^\s*(\d+)\s+(.+)$/);if(J){let U=parseInt(J[1]||"0",10),X=J[2]||"",{stdout:$}=await E(`git log --format='%aE' --author="${X.replace(/"/g,"\\\"")}" -1`,{cwd:q}),M=$.trim().split(`
4
+ `)[0]||"";z.push({name:X,email:M,commits:U})}}return z.sort((B,J)=>J.commits-B.commits)}catch(Q){throw new Error(`Failed to get contributors from ${q}: ${Q}`)}}function L(){console.log("Usage: gstatx contributors [options] <repo-path> [repo-path2 ...]"),console.log(`
5
+ List contributors for specified git repositories.`),console.log(`
6
+ Options:`),console.log(" --no-commits Hide commit counts in contributor list"),console.log(" --help, -h Show this help message"),console.log(`
7
+ Examples:`),console.log(" gstatx contributors ./my-repo"),console.log(" gstatx contributors ./repo1 ./repo2 ./repo3"),console.log(" gstatx contributors --no-commits ./my-repo")}async function R(q,Q=!0){for(let z of q){if(!await Z(z)){console.error(`Error: ${z} is not a git repository`);continue}try{let B=await v(z);if(console.log(`
8
+ \uD83D\uDCCA Contributors for ${z}:`),console.log("─".repeat(60)),B.length===0)console.log(" No contributors found");else for(let J of B)if(Q)console.log(` ${J.name} <${J.email}> (${J.commits} commits)`);else console.log(` ${J.name} <${J.email}>`)}catch(B){console.error(`Error processing ${z}:`,B)}}}import{existsSync as H}from"node:fs";import{readFile as y}from"node:fs/promises";import{dirname as I,join as b,resolve as k}from"node:path";var x=".gstatxrc.json";function A(q){let Q=k(q),z=20,K=0;while(K<z){let B=b(Q,x);if(H(B))return B;let J=I(Q);if(J===Q)break;Q=J,K++}return null}async function G(q,Q=process.cwd()){try{let z;if(q){let U=k(q);if(!H(U))return console.error(`Error: Config file not found: ${U}`),null;z=U}else if(z=A(Q),!z)return null;let K=await y(z,"utf-8"),B=JSON.parse(K),J=I(z);if(B.repositories)B.repositories=B.repositories.map((U)=>({...U,path:k(J,U.path)}));return{config:B,configDir:J}}catch(z){if(q)console.error(`Error: Failed to load config file: ${z}`);return null}}import{exec as C}from"node:child_process";import{existsSync as O}from"node:fs";import{mkdir as u}from"node:fs/promises";import{dirname as l}from"node:path";import{promisify as f}from"node:util";var P=f(C);async function _(q){if(O(q.path)&&await Z(q.path))return q.path;if(O(q.path))return console.error(`Error: Path exists but is not a git repository: ${q.path}`),null;if(!q.url)return console.error(`Error: Repository not found at ${q.path} and no URL provided for cloning`),null;try{console.log(`Cloning repository from ${q.url} to ${q.path}...`);let Q=l(q.path),z=q.path.split("/").pop()||"repository";try{await u(Q,{recursive:!0})}catch(K){}return await P(`git clone "${q.url}" "${q.path}"`),console.log(`✓ Successfully cloned ${q.name||z}`),q.path}catch(Q){return console.error(`Error: Failed to clone repository: ${Q}`),null}}function j(){console.log("Usage: gstatx [command] [options] <repo-path> [repo-path2 ...]"),console.log(`
9
+ A CLI tool to export statistics from git repositories.`),console.log(`
10
+ Commands:`),console.log(" contributors List contributors for specified repositories"),console.log(`
11
+ Global Options:`),console.log(" --help, -h Show help message"),console.log(" --config, -c <path> Specify custom config file path"),console.log(`
12
+ Configuration:`),console.log(" You can create a .gstatxrc.json file to set default options"),console.log(" and repository paths. CLI arguments override config values."),console.log(" Set 'cloneIfNotExists: true' to automatically clone repositories"),console.log(" that don't exist locally (requires 'url' in repository config)."),console.log(`
13
+ Examples:`),console.log(" gstatx contributors ./my-repo"),console.log(" gstatx contributors --help"),console.log(" gstatx --config ./custom-config.json contributors"),console.log(`
14
+ Run 'gstatx <command> --help' for command-specific help.`)}function d(q){let Q=[],z;for(let K=0;K<q.length;K++){let B=q[K];if(B==="--config"||B==="-c"){let J=q[K+1];if(!J||J.startsWith("-"))console.error(`Error: ${B} requires a config file path`),process.exit(1);z=J,K++}else Q.push(B??"")}return{configPath:z,remainingArgs:Q}}async function m(){let q=process.argv.slice(2);if(q.length===0||q[0]==="--help"||q[0]==="-h")j(),process.exit(0);let{configPath:Q,remainingArgs:z}=d(q),K=z[0],B=z.slice(1);if(!K)console.error("Error: No command or repository paths specified"),process.exit(1);if(B.includes("--help")||B.includes("-h")){if(K==="contributors")L(),process.exit(0);console.error(`Error: Unknown command "${K}"`),j(),process.exit(1)}let J=await G(Q),U=B.includes("--no-commits"),X=J?.config.contributors?.["no-commits"]??!1,$=!(U||X),V=B.filter((W)=>W!=="--no-commits"&&W!=="--help"&&W!=="-h");if(V.length===0&&J?.config.repositories){let W=J.config.repositories;if(J.config.cloneIfNotExists){let Y=[];for(let w of W){let T=await _(w);if(T)Y.push(T)}V=Y}else V=W.map((Y)=>Y.path)}if(V.length===0&&K==="contributors")console.error("Error: No repository paths specified"),console.log("Run 'gstatx contributors --help' for usage information"),console.log("Or configure repositories in .gstatxrc.json"),process.exit(1);switch(K){case"contributors":await R(V,$);break;default:if(K.startsWith("-")||K.includes("/")||K===".")console.log("Repositories to analyze:",[K,...V]);else console.error(`Error: Unknown command "${K}"`),console.log("Run 'gstatx' without arguments to see usage"),process.exit(1)}}m().catch(console.error);
package/dist/lib.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * gstatx - Library API
3
+ * Export stats of a repo or list of repos
4
+ */
5
+ import { type RepoContributor } from "./commands/contributors.js";
6
+ import type { GstatxConfig, RepositoryConfig } from "./utils/config.js";
7
+ export type * from "./types.js";
8
+ export interface ClientOptions extends GstatxConfig {
9
+ cloneIfNotExists?: boolean;
10
+ repositories?: RepositoryConfig[];
11
+ }
12
+ export declare class Client {
13
+ private config;
14
+ private loadedConfig;
15
+ constructor(options: ClientOptions);
16
+ /**
17
+ * Initialize the client by loading config file if needed
18
+ */
19
+ private initialize;
20
+ /**
21
+ * Get repository paths to use
22
+ */
23
+ private getRepoPaths;
24
+ /**
25
+ * List contributors for specified repositories
26
+ * @param repoPaths Optional array of repository paths. If not provided, uses repositories from config.
27
+ * @returns Array of contributors grouped by repository
28
+ */
29
+ listContributors(repoPaths?: string[]): Promise<Array<{
30
+ path: string;
31
+ contributors: RepoContributor[];
32
+ }>>;
33
+ }
34
+ export { getContributors } from "./commands/contributors.js";
35
+ export { loadConfig } from "./utils/config.js";
36
+ export { ensureRepository } from "./utils/repo.js";
37
+ export { isGitRepo } from "./utils/git.js";
package/dist/lib.js ADDED
@@ -0,0 +1,3 @@
1
+ import{exec as E}from"node:child_process";import{promisify as N}from"node:util";import{access as L}from"node:fs/promises";import{join as O}from"node:path";async function T(q){try{return await L(O(q,".git")),!0}catch{return!1}}var Z=N(E);async function U(q){try{let{stdout:z}=await Z("git shortlog -sn --all",{cwd:q}),B=[],J=z.trim().split(`
2
+ `);for(let K of J){let H=K.match(/^\s*(\d+)\s+(.+)$/);if(H){let Q=parseInt(H[1]||"0",10),Y=H[2]||"",{stdout:M}=await Z(`git log --format='%aE' --author="${Y.replace(/"/g,"\\\"")}" -1`,{cwd:q}),I=M.trim().split(`
3
+ `)[0]||"";B.push({name:Y,email:I,commits:Q})}}return B.sort((K,H)=>H.commits-K.commits)}catch(z){throw new Error(`Failed to get contributors from ${q}: ${z}`)}}import{existsSync as $}from"node:fs";import{readFile as R}from"node:fs/promises";import{dirname as k,join as A,resolve as V}from"node:path";var F=".gstatxrc.json";function _(q){let z=V(q),B=20,J=0;while(J<B){let K=A(z,F);if($(K))return K;let H=k(z);if(H===z)break;z=H,J++}return null}async function W(q,z=process.cwd()){try{let B;if(q){let Q=V(q);if(!$(Q))return console.error(`Error: Config file not found: ${Q}`),null;B=Q}else if(B=_(z),!B)return null;let J=await R(B,"utf-8"),K=JSON.parse(J),H=k(B);if(K.repositories)K.repositories=K.repositories.map((Q)=>({...Q,path:V(H,Q.path)}));return{config:K,configDir:H}}catch(B){if(q)console.error(`Error: Failed to load config file: ${B}`);return null}}import{exec as D}from"node:child_process";import{existsSync as w}from"node:fs";import{mkdir as G}from"node:fs/promises";import{dirname as S}from"node:path";import{promisify as v}from"node:util";var j=v(D);async function X(q){if(w(q.path)&&await T(q.path))return q.path;if(w(q.path))return console.error(`Error: Path exists but is not a git repository: ${q.path}`),null;if(!q.url)return console.error(`Error: Repository not found at ${q.path} and no URL provided for cloning`),null;try{console.log(`Cloning repository from ${q.url} to ${q.path}...`);let z=S(q.path),B=q.path.split("/").pop()||"repository";try{await G(z,{recursive:!0})}catch(J){}return await j(`git clone "${q.url}" "${q.path}"`),console.log(`✓ Successfully cloned ${q.name||B}`),q.path}catch(z){return console.error(`Error: Failed to clone repository: ${z}`),null}}class C{config;loadedConfig=null;constructor(q){this.config=q}async initialize(){if(!this.config.repositories){if(this.loadedConfig=await W(),this.loadedConfig?.config.repositories)this.config.repositories=this.loadedConfig.config.repositories}if(this.config.cloneIfNotExists&&this.config.repositories){let q=[];for(let z of this.config.repositories){let B=await X(z);if(B)q.push(B)}this.config.repositories=this.config.repositories.map((z,B)=>({...z,path:q[B]||z.path}))}}getRepoPaths(q){if(q&&q.length>0)return q;if(this.config.repositories)return this.config.repositories.map((z)=>z.path);return[]}async listContributors(q){await this.initialize();let z=this.getRepoPaths(q);if(z.length===0)throw new Error("No repository paths specified");let B=[];for(let J of z){if(!await T(J))throw new Error(`${J} is not a git repository`);try{let H=await U(J);B.push({path:J,contributors:H})}catch(H){throw new Error(`Failed to get contributors from ${J}: ${H}`)}}return B}}export{W as loadConfig,T as isGitRepo,U as getContributors,X as ensureRepository,C as Client};
@@ -0,0 +1,2 @@
1
+ export type { RepoContributor } from "./commands/contributors";
2
+ export type { ContributorsConfig, GstatxConfig, LoadedConfig, RepositoryConfig, } from "./utils/config";
@@ -0,0 +1,18 @@
1
+ export interface RepositoryConfig {
2
+ path: string;
3
+ name?: string;
4
+ url?: string;
5
+ }
6
+ export interface ContributorsConfig {
7
+ "no-commits"?: boolean;
8
+ }
9
+ export interface GstatxConfig {
10
+ contributors?: ContributorsConfig;
11
+ repositories?: RepositoryConfig[];
12
+ cloneIfNotExists?: boolean;
13
+ }
14
+ export interface LoadedConfig {
15
+ config: GstatxConfig;
16
+ configDir: string;
17
+ }
18
+ export declare function loadConfig(customPath?: string, startPath?: string): Promise<LoadedConfig | null>;
@@ -0,0 +1 @@
1
+ export declare function isGitRepo(path: string): Promise<boolean>;
@@ -0,0 +1,2 @@
1
+ import type { RepositoryConfig } from "./config.js";
2
+ export declare function ensureRepository(repo: RepositoryConfig): Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gstatx",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Export stats of a repo or list of repos",
5
5
  "type": "module",
6
6
  "engines": {
@@ -9,12 +9,20 @@
9
9
  "bin": {
10
10
  "gstatx": "dist/index.js"
11
11
  },
12
- "main": "dist/index.js",
12
+ "main": "dist/lib.js",
13
+ "types": "dist/lib.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/lib.d.ts",
17
+ "import": "./dist/lib.js",
18
+ "default": "./dist/lib.js"
19
+ }
20
+ },
13
21
  "files": [
14
22
  "dist"
15
23
  ],
16
24
  "scripts": {
17
- "build": "bun build src/index.ts --outdir dist --target node --minify",
25
+ "build": "bun build src/index.ts --outdir dist --target node --minify && bun build src/lib.ts --outdir dist --target node --minify && tsc -p tsconfig.build.json",
18
26
  "prepack": "bun run build",
19
27
  "start": "bun run src/index.ts",
20
28
  "lint": "biome lint .",