new-branch 0.3.1 → 0.4.0

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
@@ -4,10 +4,16 @@
4
4
  <img src="./logo.svg" width="180" alt="new-branch logo" />
5
5
  </p>
6
6
 
7
+ > “Explicit is better than implicit.”
8
+ > — The Zen of Python (PEP 20)
9
+
7
10
  A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
8
11
 
9
12
  ![demo](./demo.gif)
10
13
 
14
+ [![CI](https://github.com/teles/new-branch/actions/workflows/ci.yml/badge.svg)](https://github.com/teles/new-branch/actions/workflows/ci.yml)
15
+ [![codecov](https://codecov.io/gh/teles/new-branch/branch/main/graph/badge.svg)](https://codecov.io/gh/teles/new-branch)
16
+
11
17
  ---
12
18
 
13
19
  ## Why
@@ -82,10 +88,20 @@ Example:
82
88
 
83
89
  ## Built-in Variables
84
90
 
91
+ ### Core Variables
92
+
85
93
  - `type`
86
94
  - `title`
87
95
  - `id`
88
96
 
97
+ ### Date Built-ins (derived from local system time)
98
+
99
+ - `year` → YYYY
100
+ - `month` → MM (zero padded)
101
+ - `day` → DD (zero padded)
102
+ - `date` → YYYY-MM-DD
103
+ - `dateCompact` → YYYYMMDD
104
+
89
105
  ---
90
106
 
91
107
  ## Built-in Transforms
@@ -144,11 +160,36 @@ You can define a default pattern in `package.json`:
144
160
  }
145
161
  ```
146
162
 
147
- Resolution order:
163
+ ### Git Configuration
164
+
165
+ You can also define a default pattern using Git config:
166
+
167
+ ```bash
168
+ git config --local new-branch.pattern "{type}/{title:slugify}-{id}"
169
+ ```
170
+
171
+ Or globally:
172
+
173
+ ```bash
174
+ git config --global new-branch.pattern "{type}/{title:slugify}-{id}"
175
+ ```
176
+
177
+ To remove the pattern from Git config:
178
+
179
+ ```bash
180
+ # Remove from local repository
181
+ git config --unset --local new-branch.pattern
182
+
183
+ # Remove from global config
184
+ git config --unset --global new-branch.pattern
185
+ ```
186
+
187
+ When using Git config, the resolution order becomes:
148
188
 
149
189
  1. CLI flags
150
190
  2. `package.json` configuration
151
- 3. Interactive prompt (if enabled)
191
+ 3. Git config (`new-branch.pattern`)
192
+ 4. Interactive prompt (if enabled)
152
193
 
153
194
  ---
154
195
 
package/dist/cli.js CHANGED
@@ -4,7 +4,9 @@ import { parsePattern } from "./pattern/parsePattern.js";
4
4
  import { defaultTransforms } from "./pattern/transforms/index.js";
5
5
  import { renderPattern } from "./pattern/transforms/renderPattern.js";
6
6
  import { resolveMissingValues } from "./runtime/resolveMissingValues.js";
7
+ import { getBuiltinValues } from "./runtime/builtins.js";
7
8
  import { loadProjectConfig } from "./config/loadProjectConfig.js";
9
+ import { getGitConfig } from "./git/gitConfig.js";
8
10
  import { sanitizeGitRef } from "./git/sanitizeGitRef.js";
9
11
  import { validateBranchName } from "./git/validateBranchName.js";
10
12
  import { createBranch } from "./git/createBranch.js";
@@ -46,7 +48,7 @@ function toInitialValues(args) {
46
48
  type: args.options.type,
47
49
  };
48
50
  }
49
- async function run() {
51
+ export async function run() {
50
52
  // Step 0: args/options
51
53
  // Note: CAC prints help, but depending on our parseArgs wrapper we might not
52
54
  // expose `help` in `args.options`. We still want to exit early and never
@@ -62,14 +64,23 @@ async function run() {
62
64
  const prompt = args.options.prompt !== false;
63
65
  // Pipeline: pattern -> AST -> resolve values -> render -> sanitize -> validate -> (optional) git -> output
64
66
  const projectConfig = await loadProjectConfig();
65
- const resolvedPattern = args.options.pattern ?? projectConfig.pattern;
67
+ // Git config (respects local -> global precedence automatically)
68
+ let gitPattern;
69
+ if (!args.options.pattern && !projectConfig.pattern) {
70
+ gitPattern = await getGitConfig("new-branch.pattern");
71
+ }
72
+ const resolvedPattern = args.options.pattern ?? projectConfig.pattern ?? gitPattern;
66
73
  const patternRes = requirePattern(resolvedPattern);
67
74
  if (!isOk(patternRes))
68
75
  fail("Invalid CLI arguments.", patternRes.error);
69
76
  const astRes = safe(() => parsePattern(patternRes.value));
70
77
  if (!isOk(astRes))
71
78
  fail("Invalid pattern.", astRes.error);
72
- const initialValues = toInitialValues(args);
79
+ const builtinValues = getBuiltinValues();
80
+ const initialValues = {
81
+ ...builtinValues,
82
+ ...toInitialValues(args),
83
+ };
73
84
  const valuesRes = await safeAsync(() => resolveMissingValues(astRes.value, initialValues, {
74
85
  prompt,
75
86
  }));
@@ -106,4 +117,8 @@ async function run() {
106
117
  }
107
118
  }
108
119
  }
109
- run().catch((e) => fail("Unexpected error in CLI.", e));
120
+ // Only execute the CLI automatically when not running tests.
121
+ // In tests, `run` can be imported and called directly.
122
+ if (process.env.NODE_ENV !== "test") {
123
+ run().catch((e) => fail("Unexpected error in CLI.", e));
124
+ }
@@ -0,0 +1,11 @@
1
+ import { execa } from "execa";
2
+ export async function getGitConfig(key) {
3
+ try {
4
+ const { stdout } = await execa("git", ["config", "--get", key]);
5
+ const value = stdout.trim();
6
+ return value.length ? value : undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
@@ -1,16 +1,22 @@
1
+ import { buildRegistry } from "../../pattern/transforms/registry.js";
1
2
  import { lower } from "../../pattern/transforms/lower.js";
2
3
  import { upper } from "../../pattern/transforms/upper.js";
3
4
  import { max } from "../../pattern/transforms/max.js";
4
- // import { replace } from "./replace.js";
5
- import { slugify } from "./slugify.js";
6
- export const allTransforms = [lower, upper, max, slugify];
7
- export function buildRegistry(defs) {
8
- const registry = {};
9
- for (const d of defs) {
10
- if (registry[d.name])
11
- throw new Error(`Duplicate transform name: "${d.name}"`);
12
- registry[d.name] = d.fn;
13
- }
14
- return registry;
15
- }
5
+ import { slugify } from "../../pattern/transforms/slugify.js";
6
+ import { camel } from "../../pattern/transforms/camel.js";
7
+ import { kebab } from "../../pattern/transforms/kebab.js";
8
+ import { snake } from "../../pattern/transforms/snake.js";
9
+ import { title } from "../../pattern/transforms/title.js";
10
+ import { words } from "../../pattern/transforms/words.js";
11
+ export const allTransforms = [
12
+ lower,
13
+ upper,
14
+ max,
15
+ slugify,
16
+ camel,
17
+ kebab,
18
+ snake,
19
+ title,
20
+ words,
21
+ ];
16
22
  export const defaultTransforms = buildRegistry(allTransforms);
@@ -0,0 +1,10 @@
1
+ export function buildRegistry(defs) {
2
+ const registry = {};
3
+ for (const d of defs) {
4
+ if (registry[d.name]) {
5
+ throw new Error(`Duplicate transform name: "${d.name}"`);
6
+ }
7
+ registry[d.name] = d.fn;
8
+ }
9
+ return registry;
10
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pads a number with a leading zero when necessary.
3
+ *
4
+ * @param n - Number to pad.
5
+ * @returns A zero-padded string (e.g., 2 → "02").
6
+ */
7
+ function pad(n) {
8
+ return String(n).padStart(2, "0");
9
+ }
10
+ /**
11
+ * Returns built-in date-based variables derived from
12
+ * the local system time.
13
+ *
14
+ * This function is intentionally synchronous and pure.
15
+ * The optional `now` parameter allows deterministic testing.
16
+ *
17
+ * Provided variables:
18
+ *
19
+ * - `year` → YYYY
20
+ * - `month` → MM (zero padded)
21
+ * - `day` → DD (zero padded)
22
+ * - `date` → YYYY-MM-DD
23
+ * - `dateCompact` → YYYYMMDD
24
+ *
25
+ * @param now - Optional Date instance used to generate values.
26
+ * Defaults to the current system date/time.
27
+ * @returns An object containing built-in date variables.
28
+ *
29
+ * @example
30
+ * const builtins = getBuiltinValues(new Date("2026-02-19"));
31
+ * builtins.date; // "2026-02-19"
32
+ */
33
+ export function getBuiltinValues(now = new Date()) {
34
+ const year = String(now.getFullYear());
35
+ const month = pad(now.getMonth() + 1);
36
+ const day = pad(now.getDate());
37
+ return {
38
+ year,
39
+ month,
40
+ day,
41
+ date: `${year}-${month}-${day}`,
42
+ dateCompact: `${year}${month}${day}`,
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "new-branch",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Generate and create standardized git branch names from a pattern.",
5
5
  "keywords": [
6
6
  "git",
@@ -48,10 +48,17 @@
48
48
  "packageManager": "pnpm@10.22.0",
49
49
  "devDependencies": {
50
50
  "@eslint/js": "10.0.1",
51
+ "@semantic-release/changelog": "^6.0.3",
52
+ "@semantic-release/commit-analyzer": "^13.0.1",
53
+ "@semantic-release/git": "^10.0.1",
54
+ "@semantic-release/github": "^12.0.6",
55
+ "@semantic-release/npm": "^13.1.4",
56
+ "@semantic-release/release-notes-generator": "^14.1.0",
51
57
  "@types/node": "25.2.3",
52
58
  "@vitest/coverage-v8": "4.0.18",
53
59
  "eslint": "10.0.0",
54
60
  "prettier": "3.8.1",
61
+ "semantic-release": "^25.0.3",
55
62
  "tsc-alias": "1.8.16",
56
63
  "tsx": "4.21.0",
57
64
  "typescript": "5.9.3",