@drazenbebic/wdid 0.1.0 → 0.1.3
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 +45 -7
- package/dist/index.js +168 -17
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -41,13 +41,51 @@ If a commit doesn't reference a ticket, the Ticket column is left blank (rendere
|
|
|
41
41
|
|
|
42
42
|
## Options
|
|
43
43
|
|
|
44
|
-
| Option
|
|
45
|
-
|
|
|
46
|
-
| `[date]`
|
|
47
|
-
| `--from <date>`
|
|
48
|
-
| `--to <date>`
|
|
49
|
-
| `--author <name>`
|
|
50
|
-
| `--repo <path...>`
|
|
44
|
+
| Option | Description |
|
|
45
|
+
| -------------------------- | -------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `[date]` | A `YYYY-MM-DD` date or the literal `today`. Omit to show all history. |
|
|
47
|
+
| `--from <date>` | Start date (inclusive). |
|
|
48
|
+
| `--to <date>` | End date (inclusive). |
|
|
49
|
+
| `--author <name>` | Override the git author. Defaults to `git config user.name` (or `defaultAuthor` in config). |
|
|
50
|
+
| `--repo <path...>` | One or more repo paths to query. Defaults to `defaultRepos` in config, then the current dir. |
|
|
51
|
+
| `--format <preset>` | Ticket format: `jira`, `github`, `conventional`, or `custom`. Defaults to `jira`. |
|
|
52
|
+
| `--ticket-pattern <regex>` | Custom regex for ticket extraction. Implies `--format custom`; overrides `--format`. |
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
`wdid` looks for a config file in this order:
|
|
57
|
+
|
|
58
|
+
1. **Repo-level** (current directory and walking up): `wdid.config.{js,cjs,mjs,ts,json,yaml,yml}`, `.wdidrc{,.json,.yaml,.yml,.js,.cjs}`, or a `"wdid"` field in `package.json`.
|
|
59
|
+
2. **Global**: `~/.config/wdid/config.json` (honors `XDG_CONFIG_HOME`).
|
|
60
|
+
|
|
61
|
+
CLI flags always win. The first match in this list is used in full (configs do not merge across levels).
|
|
62
|
+
|
|
63
|
+
### Schema
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"format": "jira",
|
|
68
|
+
"customPattern": "^\\[([A-Z]+-\\d+)\\]",
|
|
69
|
+
"defaultAuthor": "Jane Doe",
|
|
70
|
+
"defaultRepos": ["~/work/api", "~/work/web"]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Field | Type | Description |
|
|
75
|
+
| --------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
76
|
+
| `format` | `"jira" \| "github" \| "conventional" \| "custom"` | Ticket extraction preset. Default `jira`. |
|
|
77
|
+
| `customPattern` | `string` | Regex used when `format` is `"custom"`. First capture group wins, else full match. |
|
|
78
|
+
| `defaultAuthor` | `string` | Used when `--author` is not passed and you want to skip the `git config` lookup. |
|
|
79
|
+
| `defaultRepos` | `string[]` | Paths to query when no `--repo` is given. `~` is expanded. |
|
|
80
|
+
|
|
81
|
+
### Format presets
|
|
82
|
+
|
|
83
|
+
| Preset | Matches | Example commit → ticket |
|
|
84
|
+
| -------------- | ------------------------------------------------ | -------------------------------------- |
|
|
85
|
+
| `jira` | `ABC-123` style (uppercase project key + digits) | `feat(ABC-123): add login` → `ABC-123` |
|
|
86
|
+
| `github` | `#123` style | `Closes #42` → `42` |
|
|
87
|
+
| `conventional` | Conventional Commit `type(scope)!` | `feat(auth)!: ...` → `feat(auth)!` |
|
|
88
|
+
| `custom` | Your `customPattern` regex | depends on the regex |
|
|
51
89
|
|
|
52
90
|
## Development
|
|
53
91
|
|
package/dist/index.js
CHANGED
|
@@ -9,16 +9,32 @@ import { promisify } from "util";
|
|
|
9
9
|
var execFileAsync = promisify(execFile);
|
|
10
10
|
var FIELD_SEP = "";
|
|
11
11
|
var RECORD_SEP = "";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
function extractTicket(message, pattern) {
|
|
13
|
+
const match = message.match(pattern);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return match[1] ?? match[0];
|
|
16
18
|
}
|
|
17
19
|
async function getGitUserName(cwd) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execFileAsync("git", ["config", "user.name"], {
|
|
22
|
+
cwd
|
|
23
|
+
});
|
|
24
|
+
const name = stdout.trim();
|
|
25
|
+
if (!name) {
|
|
26
|
+
throw new Error("empty");
|
|
27
|
+
}
|
|
28
|
+
return name;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code === "ENOENT") {
|
|
31
|
+
throw new Error("git is not installed or not on PATH", { cause: err });
|
|
32
|
+
}
|
|
33
|
+
throw new Error(
|
|
34
|
+
'could not read git user.name \u2014 set it with `git config user.name "Your Name"` or pass --author',
|
|
35
|
+
{ cause: err }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
22
38
|
}
|
|
23
39
|
async function getCommits(opts) {
|
|
24
40
|
const args = [
|
|
@@ -29,8 +45,12 @@ async function getCommits(opts) {
|
|
|
29
45
|
`--author=${opts.author}`,
|
|
30
46
|
`--pretty=format:%cs${FIELD_SEP}%s${RECORD_SEP}`
|
|
31
47
|
];
|
|
32
|
-
if (opts.from)
|
|
33
|
-
|
|
48
|
+
if (opts.from) {
|
|
49
|
+
args.push(`--after=${opts.from} 00:00`);
|
|
50
|
+
}
|
|
51
|
+
if (opts.to) {
|
|
52
|
+
args.push(`--before=${opts.to} 23:59`);
|
|
53
|
+
}
|
|
34
54
|
const { stdout } = await execFileAsync("git", args, {
|
|
35
55
|
cwd: opts.cwd,
|
|
36
56
|
maxBuffer: 32 * 1024 * 1024
|
|
@@ -39,7 +59,7 @@ async function getCommits(opts) {
|
|
|
39
59
|
const [date = "", subject = ""] = record.split(FIELD_SEP);
|
|
40
60
|
return {
|
|
41
61
|
date,
|
|
42
|
-
ticket: extractTicket(subject),
|
|
62
|
+
ticket: extractTicket(subject, opts.pattern),
|
|
43
63
|
description: subject
|
|
44
64
|
};
|
|
45
65
|
});
|
|
@@ -75,7 +95,112 @@ function renderError(message) {
|
|
|
75
95
|
return chalk.red(`error: ${message}`);
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
// src/config.ts
|
|
99
|
+
import { homedir } from "os";
|
|
100
|
+
import { readFile } from "fs/promises";
|
|
101
|
+
import { join } from "path";
|
|
102
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
103
|
+
var PRESET_PATTERNS = {
|
|
104
|
+
jira: /\b([A-Z][A-Z0-9]+-\d+)\b/,
|
|
105
|
+
github: /#(\d+)/,
|
|
106
|
+
conventional: /^(\w+(?:\([^)]+\))?!?):/
|
|
107
|
+
};
|
|
108
|
+
var VALID_FORMATS = [
|
|
109
|
+
"jira",
|
|
110
|
+
"github",
|
|
111
|
+
"conventional",
|
|
112
|
+
"custom"
|
|
113
|
+
];
|
|
114
|
+
function getTicketPattern(format, customPattern) {
|
|
115
|
+
if (format === "custom") {
|
|
116
|
+
if (!customPattern) {
|
|
117
|
+
throw new Error('format "custom" requires customPattern to be set');
|
|
118
|
+
}
|
|
119
|
+
return new RegExp(customPattern);
|
|
120
|
+
}
|
|
121
|
+
return PRESET_PATTERNS[format];
|
|
122
|
+
}
|
|
123
|
+
function expandPath(p) {
|
|
124
|
+
if (p === "~") {
|
|
125
|
+
return homedir();
|
|
126
|
+
}
|
|
127
|
+
if (p.startsWith("~/")) {
|
|
128
|
+
return join(homedir(), p.slice(2));
|
|
129
|
+
}
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
function validateConfig(raw) {
|
|
133
|
+
if (typeof raw !== "object" || raw === null) {
|
|
134
|
+
throw new Error("config must be an object");
|
|
135
|
+
}
|
|
136
|
+
const obj = raw;
|
|
137
|
+
const cfg = {};
|
|
138
|
+
if ("format" in obj) {
|
|
139
|
+
if (typeof obj.format !== "string" || !VALID_FORMATS.includes(obj.format)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`invalid format "${String(obj.format)}" \u2014 must be one of ${VALID_FORMATS.join(", ")}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
cfg.format = obj.format;
|
|
145
|
+
}
|
|
146
|
+
if ("customPattern" in obj) {
|
|
147
|
+
if (typeof obj.customPattern !== "string") {
|
|
148
|
+
throw new Error("customPattern must be a string");
|
|
149
|
+
}
|
|
150
|
+
cfg.customPattern = obj.customPattern;
|
|
151
|
+
}
|
|
152
|
+
if (cfg.format === "custom" && !cfg.customPattern) {
|
|
153
|
+
throw new Error('format "custom" requires customPattern to be set');
|
|
154
|
+
}
|
|
155
|
+
if ("defaultAuthor" in obj) {
|
|
156
|
+
if (typeof obj.defaultAuthor !== "string") {
|
|
157
|
+
throw new Error("defaultAuthor must be a string");
|
|
158
|
+
}
|
|
159
|
+
cfg.defaultAuthor = obj.defaultAuthor;
|
|
160
|
+
}
|
|
161
|
+
if ("defaultRepos" in obj) {
|
|
162
|
+
if (!Array.isArray(obj.defaultRepos) || !obj.defaultRepos.every((r) => typeof r === "string")) {
|
|
163
|
+
throw new Error("defaultRepos must be an array of strings");
|
|
164
|
+
}
|
|
165
|
+
cfg.defaultRepos = obj.defaultRepos;
|
|
166
|
+
}
|
|
167
|
+
return cfg;
|
|
168
|
+
}
|
|
169
|
+
function globalConfigPath() {
|
|
170
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
171
|
+
return join(xdgConfigHome, "wdid", "config.json");
|
|
172
|
+
}
|
|
173
|
+
async function loadGlobalConfig() {
|
|
174
|
+
try {
|
|
175
|
+
const content = await readFile(globalConfigPath(), "utf-8");
|
|
176
|
+
return validateConfig(JSON.parse(content));
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err.code === "ENOENT") {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function loadConfig(cwd) {
|
|
185
|
+
const explorer = cosmiconfig("wdid");
|
|
186
|
+
const local = await explorer.search(cwd);
|
|
187
|
+
if (local?.config) {
|
|
188
|
+
return validateConfig(local.config);
|
|
189
|
+
}
|
|
190
|
+
const global = await loadGlobalConfig();
|
|
191
|
+
if (global) {
|
|
192
|
+
return global;
|
|
193
|
+
}
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
|
|
78
197
|
// src/index.ts
|
|
198
|
+
var VALID_PRESETS = [
|
|
199
|
+
"jira",
|
|
200
|
+
"github",
|
|
201
|
+
"conventional",
|
|
202
|
+
"custom"
|
|
203
|
+
];
|
|
79
204
|
function isIsoDate(value) {
|
|
80
205
|
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
81
206
|
}
|
|
@@ -89,7 +214,27 @@ function resolveDate(input) {
|
|
|
89
214
|
return input;
|
|
90
215
|
}
|
|
91
216
|
async function run(dateArg, options) {
|
|
92
|
-
const
|
|
217
|
+
const config = await loadConfig(process.cwd());
|
|
218
|
+
let format;
|
|
219
|
+
let customPattern;
|
|
220
|
+
if (options.ticketPattern) {
|
|
221
|
+
format = "custom";
|
|
222
|
+
customPattern = options.ticketPattern;
|
|
223
|
+
} else if (options.format) {
|
|
224
|
+
if (!VALID_PRESETS.includes(options.format)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`invalid --format "${options.format}" \u2014 must be one of ${VALID_PRESETS.join(", ")}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
format = options.format;
|
|
230
|
+
customPattern = config.customPattern;
|
|
231
|
+
} else {
|
|
232
|
+
format = config.format ?? "jira";
|
|
233
|
+
customPattern = config.customPattern;
|
|
234
|
+
}
|
|
235
|
+
const pattern = getTicketPattern(format, customPattern);
|
|
236
|
+
const configRepos = config.defaultRepos?.map(expandPath) ?? [];
|
|
237
|
+
const repos = options.repo && options.repo.length > 0 ? options.repo : configRepos.length > 0 ? configRepos : [process.cwd()];
|
|
93
238
|
let from = options.from ? resolveDate(options.from) : void 0;
|
|
94
239
|
let to = options.to ? resolveDate(options.to) : void 0;
|
|
95
240
|
if (dateArg) {
|
|
@@ -99,8 +244,8 @@ async function run(dateArg, options) {
|
|
|
99
244
|
}
|
|
100
245
|
const allEntries = [];
|
|
101
246
|
for (const cwd of repos) {
|
|
102
|
-
const author = options.author ?? await getGitUserName(cwd);
|
|
103
|
-
const entries = await getCommits({ author, from, to, cwd });
|
|
247
|
+
const author = options.author ?? config.defaultAuthor ?? await getGitUserName(cwd);
|
|
248
|
+
const entries = await getCommits({ author, from, to, cwd, pattern });
|
|
104
249
|
allEntries.push(...entries);
|
|
105
250
|
}
|
|
106
251
|
allEntries.sort((a, b) => b.date.localeCompare(a.date));
|
|
@@ -111,12 +256,18 @@ async function run(dateArg, options) {
|
|
|
111
256
|
process.stdout.write(renderTable(allEntries) + "\n");
|
|
112
257
|
}
|
|
113
258
|
var program = new Command();
|
|
114
|
-
program.name("wdid").description("What did I do? \u2014 summarize your git commits as a table").argument("[date]", 'a YYYY-MM-DD date or "today"; omit to show all history').option("--from <date>", 'start date (YYYY-MM-DD or "today")').option("--to <date>", 'end date (YYYY-MM-DD or "today")').option(
|
|
259
|
+
program.name("wdid").description("What did I do? \u2014 summarize your git commits as a table").version("0.1.3", "-V, --version", "output the version number").argument("[date]", 'a YYYY-MM-DD date or "today"; omit to show all history').option("--from <date>", 'start date (YYYY-MM-DD or "today")').option("--to <date>", 'end date (YYYY-MM-DD or "today")').option(
|
|
115
260
|
"--author <name>",
|
|
116
|
-
"override the git author (defaults to git config user.name)"
|
|
261
|
+
"override the git author (defaults to git config user.name, then defaultAuthor in config)"
|
|
117
262
|
).option(
|
|
118
263
|
"--repo <path...>",
|
|
119
|
-
"one or more repo paths to query (defaults to current directory)"
|
|
264
|
+
"one or more repo paths to query (overrides defaultRepos in config; defaults to current directory)"
|
|
265
|
+
).option(
|
|
266
|
+
"--format <preset>",
|
|
267
|
+
"ticket format: jira | github | conventional | custom (default: jira, or config.format)"
|
|
268
|
+
).option(
|
|
269
|
+
"--ticket-pattern <regex>",
|
|
270
|
+
"custom regex for ticket extraction (implies --format custom; overrides --format)"
|
|
120
271
|
).action(async (dateArg, options) => {
|
|
121
272
|
try {
|
|
122
273
|
await run(dateArg, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drazenbebic/wdid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "What did I do? — summarize your git activity per day as a tidy table, grouped by JIRA ticket.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"author": "Your Name <you@example.com>",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/
|
|
18
|
+
"url": "git+https://github.com/drazenbebic/wdid.git"
|
|
19
19
|
},
|
|
20
20
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/
|
|
21
|
+
"url": "https://github.com/drazenbebic/wdid/issues"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://github.com/
|
|
23
|
+
"homepage": "https://github.com/drazenbebic/wdid#readme",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"main": "dist/index.js",
|
|
26
26
|
"bin": {
|
|
@@ -46,22 +46,36 @@
|
|
|
46
46
|
"format:check": "prettier --check .",
|
|
47
47
|
"test": "vitest run",
|
|
48
48
|
"test:watch": "vitest",
|
|
49
|
+
"prepare": "husky",
|
|
49
50
|
"prepublishOnly": "pnpm run build"
|
|
50
51
|
},
|
|
51
52
|
"publishConfig": {
|
|
52
53
|
"access": "public"
|
|
53
54
|
},
|
|
55
|
+
"lint-staged": {
|
|
56
|
+
"*.{ts,tsx,js,cjs,mjs}": [
|
|
57
|
+
"eslint --fix",
|
|
58
|
+
"prettier --write"
|
|
59
|
+
],
|
|
60
|
+
"*.{json,md,yml,yaml}": [
|
|
61
|
+
"prettier --write"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
54
64
|
"dependencies": {
|
|
55
65
|
"chalk": "^5.6.2",
|
|
56
66
|
"cli-table3": "^0.6.5",
|
|
57
|
-
"commander": "^14.0.3"
|
|
67
|
+
"commander": "^14.0.3",
|
|
68
|
+
"cosmiconfig": "^9.0.1"
|
|
58
69
|
},
|
|
59
70
|
"devDependencies": {
|
|
60
71
|
"@eslint/js": "^10.0.1",
|
|
72
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
61
73
|
"@types/node": "^20.19.41",
|
|
62
74
|
"eslint": "^10.4.0",
|
|
63
75
|
"eslint-config-prettier": "^10.1.8",
|
|
64
76
|
"globals": "^17.6.0",
|
|
77
|
+
"husky": "^9.1.7",
|
|
78
|
+
"lint-staged": "^17.0.5",
|
|
65
79
|
"prettier": "^3.8.3",
|
|
66
80
|
"tsup": "^8.5.1",
|
|
67
81
|
"typescript": "^6.0.3",
|