new-branch 0.6.0 → 0.8.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.
Files changed (44) hide show
  1. package/README.md +27 -211
  2. package/dist/cli.js +151 -7
  3. package/dist/config/loadConfig.js +28 -14
  4. package/dist/config/sources/git.loader.js +46 -2
  5. package/dist/config/sources/packageJson.loader.js +13 -0
  6. package/dist/config/sources/rc.loader.js +15 -0
  7. package/dist/config/types.js +6 -4
  8. package/dist/config/validate.js +76 -8
  9. package/dist/didactic/explain.js +94 -0
  10. package/dist/didactic/listTransforms.js +25 -0
  11. package/dist/didactic/printConfig.js +28 -0
  12. package/dist/git/createBranch.js +10 -3
  13. package/dist/git/gitBuiltins.js +73 -6
  14. package/dist/git/gitConfig.js +52 -0
  15. package/dist/git/sanitizeGitRef.js +23 -5
  16. package/dist/git/truncateEnd.js +31 -0
  17. package/dist/git/validateBranchName.js +12 -3
  18. package/dist/parseArgs.js +42 -0
  19. package/dist/pattern/parsePattern.js +14 -0
  20. package/dist/pattern/transforms/after.js +27 -0
  21. package/dist/pattern/transforms/before.js +27 -0
  22. package/dist/pattern/transforms/camel.js +13 -7
  23. package/dist/pattern/transforms/helpers/words.js +6 -0
  24. package/dist/pattern/transforms/ifEmpty.js +27 -0
  25. package/dist/pattern/transforms/index.js +24 -0
  26. package/dist/pattern/transforms/kebab.js +10 -4
  27. package/dist/pattern/transforms/lower.js +13 -0
  28. package/dist/pattern/transforms/max.js +15 -0
  29. package/dist/pattern/transforms/registry.js +13 -0
  30. package/dist/pattern/transforms/remove.js +25 -0
  31. package/dist/pattern/transforms/renderPattern.js +9 -0
  32. package/dist/pattern/transforms/replace.js +25 -0
  33. package/dist/pattern/transforms/replaceAll.js +25 -0
  34. package/dist/pattern/transforms/slugify.js +18 -0
  35. package/dist/pattern/transforms/snake.js +10 -4
  36. package/dist/pattern/transforms/stripAccents.js +24 -0
  37. package/dist/pattern/transforms/title.js +11 -4
  38. package/dist/pattern/transforms/types.js +5 -0
  39. package/dist/pattern/transforms/upper.js +13 -0
  40. package/dist/pattern/transforms/words.js +11 -7
  41. package/dist/pattern/types.js +5 -0
  42. package/dist/runtime/builtins.js +5 -0
  43. package/dist/runtime/enums.js +14 -0
  44. package/package.json +6 -2
package/README.md CHANGED
@@ -4,28 +4,19 @@
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
-
10
- A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
7
+ A composable CLI to generate and create standardized Git branch names using a pattern + transform pipeline.
11
8
 
12
9
  ![demo](./demo.gif)
13
10
 
14
11
  [![CI](https://github.com/teles/new-branch/actions/workflows/ci.yml/badge.svg)](https://github.com/teles/new-branch/actions/workflows/ci.yml)
15
12
  [![codecov](https://codecov.io/gh/teles/new-branch/branch/main/graph/badge.svg)](https://codecov.io/gh/teles/new-branch)
16
13
 
17
- ---
18
-
19
- ## Why
20
-
21
- Keep branch names consistent across your team using a declarative pattern language.
14
+ 📖 **[Full Documentation](https://teles.github.io/new-branch/)**
22
15
 
23
16
  ---
24
17
 
25
18
  ## Install
26
19
 
27
- Run without installing:
28
-
29
20
  ```bash
30
21
  npx new-branch
31
22
  ```
@@ -36,227 +27,54 @@ Or install globally:
36
27
  npm install -g new-branch
37
28
  ```
38
29
 
39
- ---
40
-
41
- ## Usage
42
-
43
- Generate a branch name:
30
+ ## Quick Start
44
31
 
45
32
  ```bash
46
33
  new-branch \
47
34
  --pattern "{type}/{title:slugify;max:25}-{id}" \
48
35
  --type feat \
49
- --title "My task" \
50
- --id STK-123
51
- ```
52
-
53
- Create the branch automatically:
54
-
55
- ```bash
56
- new-branch \
57
- --pattern "{type}/{title:slugify}-{id}" \
58
- --type feat \
59
- --title "My task" \
60
- --id STK-123 \
36
+ --title "Add login page" \
37
+ --id PROJ-123 \
61
38
  --create
62
39
  ```
63
40
 
64
- ---
65
-
66
- ## Pattern Language
67
-
68
- Patterns are composed of variables and ordered transforms.
69
-
70
- Example:
71
-
72
- ```
73
- {type}/{title:slugify;max:25}-{id}
74
- ```
75
-
76
- ### Syntax
77
-
78
41
  ```
79
- {variable:transform1;transform2:arg}
42
+ ✅ Branch created and switched to: feat/add-login-page-PROJ-123
80
43
  ```
81
44
 
82
- - Variables are wrapped in `{}`
83
- - Transforms run left-to-right
84
- - Multiple transforms are separated by `;`
85
- - Transform arguments use `:`
86
-
87
- ---
88
-
89
- ## Built-in Variables
90
-
91
- ### Core Variables
92
-
93
- - `type`
94
- - `title`
95
- - `id`
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
-
105
- ### Git Built-ins (derived from current Git repository)
106
-
107
- - `currentBranch` → Current Git branch name (e.g. `main`, `feature/PROJ-123`)
108
- - `shortSha` → Short SHA of `HEAD` (e.g. `a1b2c3d`)
109
- - `repoName` → Repository directory name
110
- - `userName` → Git user name (`git config user.name`)
111
- - `lastTag` → Most recent Git tag (`git describe --tags --abbrev=0`)
112
-
113
- > Note:
114
- >
115
- > - Git built-ins are resolved lazily and only when referenced in the pattern.
116
- > - They are never prompted interactively.
117
- > - When unavailable (e.g. outside a Git repository), they resolve to an empty string.
118
-
119
- #### Example with Git built-ins
120
-
121
- ```bash
122
- new-branch \
123
- --pattern "{currentBranch}-{shortSha}-{type}-{title:slugify}" \
124
- --type feat \
125
- --title "Improve logging"
126
- ```
127
-
128
- Example output:
129
-
130
- ```
131
- main-a1b2c3d-feat-improve-logging
132
- ```
133
-
134
- ---
135
-
136
- ## Built-in Transforms
137
-
138
- | Transform | Description |
139
- | --------- | -------------------------- |
140
- | `slugify` | Convert to URL-safe slug |
141
- | `lower` | Convert to lowercase |
142
- | `upper` | Convert to uppercase |
143
- | `camel` | Convert to camelCase |
144
- | `kebab` | Convert to kebab-case |
145
- | `snake` | Convert to snake_case |
146
- | `title` | Convert to Title Case |
147
- | `words:n` | Keep at most `n` words |
148
- | `max:n` | Truncate to `n` characters |
149
-
150
- All transforms are pure functions and composable.
151
-
152
- ---
153
-
154
- ## Interactive Mode
155
-
156
- If variables referenced by the pattern are missing, the CLI prompts for them by default.
157
-
158
- Disable prompts with:
159
-
160
- ```bash
161
- --no-prompt
162
- ```
163
-
164
- ---
165
-
166
- ## CLI Options
167
-
168
- | Option | Description |
169
- | ------------------------- | ----------------------------------- |
170
- | `-p, --pattern <pattern>` | Branch pattern |
171
- | `--type <type>` | Branch type |
172
- | `--title <title>` | Task title |
173
- | `--id <id>` | Task identifier |
174
- | `--create` | Create branch using `git switch -c` |
175
- | `--no-prompt` | Fail instead of prompting |
176
- | `--quiet` | Suppress output |
177
-
178
- ---
179
-
180
- ## Project Configuration and precedence
181
-
182
- Configuration for `new-branch` may come from several places. The CLI resolves the first _non-empty_ configuration it finds according to the following precedence (highest → lowest):
183
-
184
- 1. CLI flags (explicit `--pattern`, `--type`, etc.)
185
- 2. `.newbranchrc.json` (a repository-local JSON config file)
186
- 3. `package.json` under the `new-branch` key
187
- 4. Git config (`new-branch.pattern`) — local then global
188
- 5. Interactive prompt (only if enabled and a value is still missing)
189
-
190
- This means that if a higher-precedence source provides a non-empty value, lower-precedence sources are not consulted or merged.
191
-
192
- Examples
193
-
194
- 1. `.newbranchrc.json` (preferred when present and non-empty):
45
+ Save your pattern so you don't have to type it every time:
195
46
 
196
47
  ```json
197
48
  {
198
49
  "pattern": "{type}/{title:slugify}-{id}",
199
50
  "types": [
200
51
  { "value": "feat", "label": "Feature" },
201
- { "value": "fix", "label": "Fix" }
202
- ],
203
- "defaultType": "feat"
204
- }
205
- ```
206
-
207
- 2. `package.json` fallback:
208
-
209
- ```json
210
- {
211
- "new-branch": {
212
- "pattern": "{type}/{title:slugify}-{id}",
213
- "defaultType": "fix"
214
- }
52
+ { "value": "fix", "label": "Bug Fix" }
53
+ ]
215
54
  }
216
55
  ```
217
56
 
218
- 3. Git config fallback (local takes precedence over global):
219
-
220
- ```bash
221
- git config --local new-branch.pattern "{type}/{title:slugify}-{id}"
222
- git config --global new-branch.pattern "{type}/{title:slugify}-{id}"
223
- ```
224
-
225
- Notes about `type` and `defaultType`
226
-
227
- - Order for resolving the branch `type` follows the SPEC behavior we implemented:
228
- 1. CLI `--type` (explicit flag) overrides everything.
229
- 2. `defaultType` from the selected configuration source is used next (if present).
230
- 3. If the project config declares exactly one `type` in `types[]`, that single type is used as a convenience.
231
- 4. If the type is still not resolved and interactive prompting is allowed, the CLI will prompt for it.
232
- 5. If the type is still missing and `--no-prompt` (or `prompt: false`) is in effect, the CLI will fail with a helpful error.
57
+ ## Features
233
58
 
234
- - Validation: when a configuration source provides both `types[]` and `defaultType`, the `defaultType` must match one of the declared `types[].value`. If it does not, configuration validation will surface an error.
59
+ - **Pattern language** declarative syntax with variables, transforms, and arguments
60
+ - **16 built-in transforms** — `slugify`, `kebab`, `camel`, `max`, `replace`, `stripAccents`, and more
61
+ - **Flexible config** — `.newbranchrc.json`, `package.json`, or `git config`
62
+ - **Pattern aliases** — define named patterns and switch with `--use feature`
63
+ - **Interactive mode** — prompts for missing values, disable with `--no-prompt`
64
+ - **Git safety** — sanitized and validated via `git check-ref-format`
65
+ - **Didactic modes** — `--explain`, `--list-transforms`, `--print-config`
235
66
 
236
- - Interactive prompts: when `types[]` are present in the chosen project config, those entries are exposed as choices to the interactive `type` select prompt so users see and can pick project-defined types.
67
+ ## Documentation
237
68
 
238
- To remove the pattern from Git config:
239
-
240
- ```bash
241
- # Remove from local repository
242
- git config --unset --local new-branch.pattern
243
-
244
- # Remove from global config
245
- git config --unset --global new-branch.pattern
246
- ```
247
-
248
- ---
249
-
250
- ## Git Safety
251
-
252
- After rendering, branch names are:
253
-
254
- 1. Lightly sanitized
255
- 2. Validated via `git check-ref-format --branch`
256
-
257
- Invalid names cause the command to fail.
258
-
259
- ---
69
+ | Section | Description |
70
+ | --------------------------------------------------------------------------- | ------------------------------------ |
71
+ | [Getting Started](https://teles.github.io/new-branch/guide/getting-started) | Installation and first branch |
72
+ | [Patterns](https://teles.github.io/new-branch/guide/patterns) | Pattern language syntax and examples |
73
+ | [Transforms](https://teles.github.io/new-branch/guide/transforms) | All 16 transforms with I/O tables |
74
+ | [Configuration](https://teles.github.io/new-branch/guide/configuration) | Config sources and precedence |
75
+ | [Pattern Aliases](https://teles.github.io/new-branch/guide/pattern-aliases) | Named patterns with `--use` |
76
+ | [CLI Reference](https://teles.github.io/new-branch/reference/cli-options) | All flags and options |
77
+ | [Recipes](https://teles.github.io/new-branch/recipes/github-flow) | GitHub Flow, Gitflow, Monorepo |
260
78
 
261
79
  ## Development
262
80
 
@@ -266,8 +84,6 @@ pnpm test:run
266
84
  pnpm build
267
85
  ```
268
86
 
269
- ---
270
-
271
87
  ## License
272
88
 
273
89
  MIT
package/dist/cli.js CHANGED
@@ -1,25 +1,68 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * @module cli
4
+ *
5
+ * Entry point for the `new-branch` CLI.
6
+ *
7
+ * @remarks
8
+ * This module orchestrates the full branch-name pipeline:
9
+ * pattern → AST → resolve values → render → sanitize →
10
+ * truncate → validate → (optional) git create → output.
11
+ */
2
12
  import { parseArgs } from "./parseArgs.js";
3
13
  import { parsePattern } from "./pattern/parsePattern.js";
4
- import { defaultTransforms } from "./pattern/transforms/index.js";
14
+ import { allTransforms, defaultTransforms } from "./pattern/transforms/index.js";
5
15
  import { renderPattern } from "./pattern/transforms/renderPattern.js";
6
16
  import { resolveMissingValues } from "./runtime/resolveMissingValues.js";
7
17
  import { getBuiltinValues } from "./runtime/builtins.js";
8
- import { loadConfig } from "./config/loadConfig.js";
18
+ import { loadConfigWithSource } from "./config/loadConfig.js";
9
19
  import { getGitConfig } from "./git/gitConfig.js";
10
20
  import { extractGitBuiltinKeysFromPattern, getGitBuiltins, patternNeedsGitBuiltins, } from "./git/gitBuiltins.js";
11
21
  import { sanitizeGitRef } from "./git/sanitizeGitRef.js";
22
+ import { truncateEnd } from "./git/truncateEnd.js";
12
23
  import { validateBranchName } from "./git/validateBranchName.js";
13
24
  import { createBranch } from "./git/createBranch.js";
25
+ import { listTransforms } from "./didactic/listTransforms.js";
26
+ import { printConfig } from "./didactic/printConfig.js";
27
+ import { explain } from "./didactic/explain.js";
28
+ /**
29
+ * Wraps a value in an {@link Ok} result.
30
+ *
31
+ * @typeParam T - Value type.
32
+ * @param value - The success value.
33
+ * @returns An `Ok<T>` result.
34
+ */
14
35
  const ok = (value) => ({ ok: true, value });
36
+ /**
37
+ * Wraps an error in an {@link Err} result.
38
+ *
39
+ * @param error - The failure reason.
40
+ * @returns An `Err` result.
41
+ */
15
42
  const err = (error) => ({ ok: false, error });
43
+ /**
44
+ * Type-narrowing guard that checks whether `r` is an {@link Ok} result.
45
+ */
16
46
  const isOk = (r) => r.ok;
47
+ /**
48
+ * Prints an error message and exits the process with code 1.
49
+ *
50
+ * @param msg - Human-readable error headline.
51
+ * @param e - Optional underlying error to print.
52
+ */
17
53
  function fail(msg, e) {
18
54
  console.error(`\n❌ ${msg}`);
19
55
  if (e)
20
56
  console.error(e);
21
57
  process.exit(1);
22
58
  }
59
+ /**
60
+ * Wraps a synchronous function in a {@link Result}.
61
+ *
62
+ * @typeParam T - Return type of `fn`.
63
+ * @param fn - The function to execute.
64
+ * @returns `Ok<T>` on success, `Err` on thrown exception.
65
+ */
23
66
  function safe(fn) {
24
67
  try {
25
68
  return ok(fn());
@@ -28,6 +71,13 @@ function safe(fn) {
28
71
  return err(e);
29
72
  }
30
73
  }
74
+ /**
75
+ * Wraps an asynchronous function in a {@link Result}.
76
+ *
77
+ * @typeParam T - Resolved type of the returned promise.
78
+ * @param fn - The async function to execute.
79
+ * @returns `Ok<T>` on success, `Err` on rejection.
80
+ */
31
81
  async function safeAsync(fn) {
32
82
  try {
33
83
  return ok(await fn());
@@ -36,12 +86,25 @@ async function safeAsync(fn) {
36
86
  return err(e);
37
87
  }
38
88
  }
89
+ /**
90
+ * Validates that a pattern string was provided.
91
+ *
92
+ * @param pattern - The resolved pattern string (may be `undefined`).
93
+ * @returns `Ok<string>` when valid, `Err` when missing or blank.
94
+ */
39
95
  function requirePattern(pattern) {
40
96
  if (!pattern || pattern.trim().length === 0) {
41
97
  return err(new Error("Pattern is required. Use --pattern to specify it."));
42
98
  }
43
99
  return ok(pattern);
44
100
  }
101
+ /**
102
+ * Extracts CLI-provided values (`id`, `title`, `type`) from parsed arguments
103
+ * into a {@link RenderValues} map.
104
+ *
105
+ * @param args - The parsed CLI arguments.
106
+ * @returns A partial {@link RenderValues} map.
107
+ */
45
108
  function toInitialValues(args) {
46
109
  return {
47
110
  id: args.options.id,
@@ -49,6 +112,25 @@ function toInitialValues(args) {
49
112
  type: args.options.type,
50
113
  };
51
114
  }
115
+ /**
116
+ * Main CLI entry point.
117
+ *
118
+ * @remarks
119
+ * Orchestrates the full branch-name pipeline:
120
+ *
121
+ * 1. Parse and normalise `process.argv`.
122
+ * 2. Load project configuration (RC / `package.json` / git config).
123
+ * 3. Resolve the pattern (CLI flag → `--use` alias → config → git config).
124
+ * 4. Parse the pattern into an AST.
125
+ * 5. Resolve built-in and git built-in values.
126
+ * 6. Prompt for any missing values (unless `--no-prompt`).
127
+ * 7. Render, sanitize, truncate (`--max-length`), and validate.
128
+ * 8. Optionally create the branch (`--create`).
129
+ * 9. Print the branch name (unless `--quiet`).
130
+ *
131
+ * Didactic modes (`--help`, `--list-transforms`, `--print-config`,
132
+ * `--explain`) short-circuit the pipeline and return early.
133
+ */
52
134
  export async function run() {
53
135
  // Normalize argv so it works consistently across:
54
136
  // - node dist/cli.js --id 123
@@ -74,17 +156,52 @@ export async function run() {
74
156
  if (wantsHelp) {
75
157
  return;
76
158
  }
159
+ // --- Didactic mode: --list-transforms ---
160
+ if (args.options.listTransforms) {
161
+ console.log(listTransforms(allTransforms));
162
+ return;
163
+ }
77
164
  const quiet = args.options.quiet === true;
78
165
  const create = args.options.create === true;
79
166
  const prompt = args.options.prompt !== false;
167
+ const isExplain = args.options.explain === true;
80
168
  // Pipeline: pattern -> AST -> resolve values -> render -> sanitize -> validate -> (optional) git -> output
81
- const projectConfig = await loadConfig();
169
+ const { config: projectConfig, source: configSource } = await loadConfigWithSource();
170
+ // --- Didactic mode: --print-config ---
171
+ if (args.options.printConfig) {
172
+ console.log(printConfig(projectConfig, configSource));
173
+ return;
174
+ }
82
175
  // Git config (respects local -> global precedence automatically)
83
176
  let gitPattern;
84
- if (!args.options.pattern && !projectConfig.pattern) {
177
+ if (!args.options.pattern && !args.options.use && !projectConfig.pattern) {
85
178
  gitPattern = await getGitConfig("new-branch.pattern");
86
179
  }
87
- const resolvedPattern = args.options.pattern ?? projectConfig.pattern ?? gitPattern;
180
+ // Resolution logic for --use:
181
+ // If --pattern is provided, use it directly (highest precedence).
182
+ // If --use is provided, resolve from patterns config. Error if alias not found.
183
+ // Otherwise, fall back to configured pattern or git config.
184
+ let useAliasPattern;
185
+ if (!args.options.pattern && args.options.use) {
186
+ const aliasName = args.options.use;
187
+ const aliasPattern = projectConfig.patterns?.[aliasName];
188
+ if (!aliasPattern) {
189
+ fail(`Unknown pattern alias "${aliasName}". Available aliases: ${projectConfig.patterns
190
+ ? Object.keys(projectConfig.patterns).join(", ")
191
+ : "(none configured)"}`);
192
+ }
193
+ useAliasPattern = aliasPattern;
194
+ }
195
+ const resolvedPattern = args.options.pattern ?? useAliasPattern ?? projectConfig.pattern ?? gitPattern;
196
+ const patternSource = args.options.pattern
197
+ ? "CLI --pattern"
198
+ : useAliasPattern
199
+ ? `CLI --use (${args.options.use})`
200
+ : projectConfig.pattern
201
+ ? configSource
202
+ : gitPattern
203
+ ? "git config"
204
+ : "(none)";
88
205
  const patternRes = requirePattern(resolvedPattern);
89
206
  if (!isOk(patternRes))
90
207
  fail("Invalid CLI arguments.", patternRes.error);
@@ -135,7 +252,34 @@ export async function run() {
135
252
  if (!isOk(renderedRes))
136
253
  fail("Failed to render branch name.", renderedRes.error);
137
254
  const sanitized = sanitizeGitRef(renderedRes.value);
138
- const validateRes = await safeAsync(() => validateBranchName(sanitized));
255
+ // Apply --max-length truncation (after sanitization, before validation)
256
+ const maxLength = args.options.maxLength;
257
+ let finalBranchName = sanitized;
258
+ if (maxLength !== undefined) {
259
+ const truncateRes = safe(() => truncateEnd(sanitized, maxLength));
260
+ if (!isOk(truncateRes))
261
+ fail("Invalid --max-length value.", truncateRes.error);
262
+ finalBranchName = truncateRes.value;
263
+ }
264
+ // --- Didactic mode: --explain ---
265
+ if (isExplain) {
266
+ console.log(explain({
267
+ pattern: patternRes.value,
268
+ patternSource,
269
+ ast: astRes.value,
270
+ resolvedValues: valuesRes.value,
271
+ cliValues: toInitialValues(args),
272
+ builtinValues,
273
+ gitValues,
274
+ rendered: renderedRes.value,
275
+ sanitized,
276
+ maxLength,
277
+ truncated: finalBranchName,
278
+ transforms: defaultTransforms,
279
+ }));
280
+ return;
281
+ }
282
+ const validateRes = await safeAsync(() => validateBranchName(finalBranchName));
139
283
  if (!isOk(validateRes))
140
284
  fail("Branch name is not valid for git.", validateRes.error);
141
285
  const ctx = {
@@ -145,7 +289,7 @@ export async function run() {
145
289
  pattern: patternRes.value,
146
290
  ast: astRes.value,
147
291
  values: valuesRes.value,
148
- branchName: sanitized,
292
+ branchName: finalBranchName,
149
293
  };
150
294
  const createRes = create ? await safeAsync(() => createBranch(ctx.branchName)) : ok(undefined);
151
295
  if (!isOk(createRes))
@@ -1,31 +1,45 @@
1
1
  /**
2
- * @fileoverview
2
+ * @module config/loadConfig
3
+ *
3
4
  * Aggregates configuration sources without merging.
4
5
  *
5
- * Precedence:
6
- * 1) .new-branchrc.json
7
- * 2) package.json
8
- * 3) git config
6
+ * @remarks
7
+ * Precedence (first-found wins):
8
+ * 1. `.new-branchrc.json`
9
+ * 2. `package.json`
10
+ * 3. `git config`
9
11
  */
10
12
  import { rcLoader } from "./sources/rc.loader.js";
11
13
  import { packageJsonLoader } from "./sources/packageJson.loader.js";
12
14
  import { gitLoader } from "./sources/git.loader.js";
13
15
  /**
14
- * Loads the first configuration found.
15
- * No merging is performed.
16
+ * Loads the first configuration found across all supported sources.
17
+ *
18
+ * @remarks
19
+ * No merging is performed — the first source that returns a
20
+ * non-empty config wins.
21
+ *
22
+ * @returns The resolved {@link ProjectConfig}.
16
23
  */
17
24
  export async function loadConfig() {
18
- // Load rc loader first and prefer a non-empty config. Avoid calling the
19
- // git loader unless necessary because it depends on external git state
20
- // and may import modules that are platform-sensitive.
25
+ const { config } = await loadConfigWithSource();
26
+ return config;
27
+ }
28
+ /**
29
+ * Loads the first configuration found and reports which source provided it.
30
+ *
31
+ * @returns An object with the resolved {@link ProjectConfig} and a
32
+ * human-readable `source` label (e.g. `".newbranchrc.json"`).
33
+ */
34
+ export async function loadConfigWithSource() {
21
35
  const rcRes = await rcLoader.load();
22
36
  if (rcRes.found && rcRes.config && Object.keys(rcRes.config).length > 0)
23
- return rcRes.config;
37
+ return { config: rcRes.config, source: ".newbranchrc.json" };
24
38
  const pkgRes = await packageJsonLoader.load();
25
39
  if (pkgRes.found && pkgRes.config && Object.keys(pkgRes.config).length > 0)
26
- return pkgRes.config;
40
+ return { config: pkgRes.config, source: "package.json" };
27
41
  const gitRes = await gitLoader.load();
28
42
  if (gitRes.found)
29
- return gitRes.config ?? {};
30
- return {};
43
+ return { config: gitRes.config ?? {}, source: "git config" };
44
+ return { config: {}, source: "(none)" };
31
45
  }
@@ -1,5 +1,15 @@
1
- import { getGitConfig } from "../../git/gitConfig.js";
1
+ import { getGitConfig, getGitConfigRegexp } from "../../git/gitConfig.js";
2
2
  import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
3
+ /**
4
+ * Parses a comma-separated git config value into {@link BranchType} entries.
5
+ *
6
+ * @remarks
7
+ * Each entry may be either `"value"` (label defaults to value) or
8
+ * `"value:label"` (colon-separated).
9
+ *
10
+ * @param raw - The raw comma-separated string from git config.
11
+ * @returns An array of {@link BranchType} objects.
12
+ */
3
13
  function parseGitTypes(raw) {
4
14
  return raw
5
15
  .split(",")
@@ -16,13 +26,44 @@ function parseGitTypes(raw) {
16
26
  };
17
27
  });
18
28
  }
29
+ /** Prefix used for per-alias pattern keys in git config. */
30
+ const PATTERNS_PREFIX = "new-branch.patterns.";
31
+ /**
32
+ * Converts raw `git config --get-regexp` entries into a patterns map.
33
+ *
34
+ * @param entries - Key/value tuples from `getGitConfigRegexp`.
35
+ * @returns A record of pattern aliases, or `undefined` when empty.
36
+ */
37
+ function parseGitPatterns(entries) {
38
+ if (entries.length === 0)
39
+ return undefined;
40
+ const result = {};
41
+ for (const [key, value] of entries) {
42
+ const name = key.slice(PATTERNS_PREFIX.length);
43
+ if (name && value) {
44
+ result[name] = value;
45
+ }
46
+ }
47
+ return Object.keys(result).length > 0 ? result : undefined;
48
+ }
49
+ /**
50
+ * Config loader that reads `new-branch.*` keys from git config.
51
+ *
52
+ * @remarks
53
+ * Supported keys:
54
+ * - `new-branch.pattern`
55
+ * - `new-branch.defaultType`
56
+ * - `new-branch.types` (comma-separated)
57
+ * - `new-branch.patterns.<alias>` (named patterns)
58
+ */
19
59
  export const gitLoader = {
20
60
  source: "git",
21
61
  async load() {
22
62
  const pattern = await getGitConfig("new-branch.pattern");
23
63
  const defaultType = await getGitConfig("new-branch.defaultType");
24
64
  const typesRaw = await getGitConfig("new-branch.types");
25
- if (!pattern && !defaultType && !typesRaw) {
65
+ const patternsEntries = await getGitConfigRegexp("^new-branch\\.patterns\\.");
66
+ if (!pattern && !defaultType && !typesRaw && patternsEntries.length === 0) {
26
67
  return { found: false, source: "git", config: undefined };
27
68
  }
28
69
  const cfg = {};
@@ -32,6 +73,9 @@ export const gitLoader = {
32
73
  cfg.defaultType = defaultType;
33
74
  if (typesRaw)
34
75
  cfg.types = parseGitTypes(typesRaw);
76
+ const patterns = parseGitPatterns(patternsEntries);
77
+ if (patterns)
78
+ cfg.patterns = patterns;
35
79
  const sourceValidated = validateProjectConfigSource(cfg, "git config");
36
80
  const finalValidated = validateProjectConfigFinal(sourceValidated, "git config");
37
81
  return { found: true, source: "git", config: finalValidated };
@@ -1,9 +1,22 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
4
+ /**
5
+ * Type guard for Node.js filesystem errors with a `code` property.
6
+ *
7
+ * @param e - The caught error value.
8
+ * @returns `true` if `e` has a `code` property.
9
+ */
4
10
  function isNodeFsError(e) {
5
11
  return typeof e === "object" && e !== null && "code" in e;
6
12
  }
13
+ /**
14
+ * Config loader that reads the `"new-branch"` key from `package.json`.
15
+ *
16
+ * @remarks
17
+ * Returns `found: false` when `package.json` does not exist or
18
+ * does not contain a `"new-branch"` key.
19
+ */
7
20
  export const packageJsonLoader = {
8
21
  source: "package.json",
9
22
  async load() {