@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.
Files changed (3) hide show
  1. package/README.md +45 -7
  2. package/dist/index.js +168 -17
  3. 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 | 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`. |
50
- | `--repo <path...>` | One or more repo paths to query. Defaults to the current directory. |
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
- var JIRA_TICKET_RE = /\b([A-Z][A-Z0-9]+-\d+)\b/;
13
- function extractTicket(message) {
14
- const match = message.match(JIRA_TICKET_RE);
15
- return match ? match[1] : null;
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
- const { stdout } = await execFileAsync("git", ["config", "user.name"], {
19
- cwd
20
- });
21
- return stdout.trim();
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) args.push(`--after=${opts.from} 00:00`);
33
- if (opts.to) args.push(`--before=${opts.to} 23:59`);
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 repos = options.repo && options.repo.length > 0 ? options.repo : [process.cwd()];
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.0",
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/your-org/wdid.git"
18
+ "url": "git+https://github.com/drazenbebic/wdid.git"
19
19
  },
20
20
  "bugs": {
21
- "url": "https://github.com/your-org/wdid/issues"
21
+ "url": "https://github.com/drazenbebic/wdid/issues"
22
22
  },
23
- "homepage": "https://github.com/your-org/wdid#readme",
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",