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.
- package/README.md +27 -211
- package/dist/cli.js +151 -7
- package/dist/config/loadConfig.js +28 -14
- package/dist/config/sources/git.loader.js +46 -2
- package/dist/config/sources/packageJson.loader.js +13 -0
- package/dist/config/sources/rc.loader.js +15 -0
- package/dist/config/types.js +6 -4
- package/dist/config/validate.js +76 -8
- package/dist/didactic/explain.js +94 -0
- package/dist/didactic/listTransforms.js +25 -0
- package/dist/didactic/printConfig.js +28 -0
- package/dist/git/createBranch.js +10 -3
- package/dist/git/gitBuiltins.js +73 -6
- package/dist/git/gitConfig.js +52 -0
- package/dist/git/sanitizeGitRef.js +23 -5
- package/dist/git/truncateEnd.js +31 -0
- package/dist/git/validateBranchName.js +12 -3
- package/dist/parseArgs.js +42 -0
- package/dist/pattern/parsePattern.js +14 -0
- package/dist/pattern/transforms/after.js +27 -0
- package/dist/pattern/transforms/before.js +27 -0
- package/dist/pattern/transforms/camel.js +13 -7
- package/dist/pattern/transforms/helpers/words.js +6 -0
- package/dist/pattern/transforms/ifEmpty.js +27 -0
- package/dist/pattern/transforms/index.js +24 -0
- package/dist/pattern/transforms/kebab.js +10 -4
- package/dist/pattern/transforms/lower.js +13 -0
- package/dist/pattern/transforms/max.js +15 -0
- package/dist/pattern/transforms/registry.js +13 -0
- package/dist/pattern/transforms/remove.js +25 -0
- package/dist/pattern/transforms/renderPattern.js +9 -0
- package/dist/pattern/transforms/replace.js +25 -0
- package/dist/pattern/transforms/replaceAll.js +25 -0
- package/dist/pattern/transforms/slugify.js +18 -0
- package/dist/pattern/transforms/snake.js +10 -4
- package/dist/pattern/transforms/stripAccents.js +24 -0
- package/dist/pattern/transforms/title.js +11 -4
- package/dist/pattern/transforms/types.js +5 -0
- package/dist/pattern/transforms/upper.js +13 -0
- package/dist/pattern/transforms/words.js +11 -7
- package/dist/pattern/types.js +5 -0
- package/dist/runtime/builtins.js +5 -0
- package/dist/runtime/enums.js +14 -0
- 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
|
-
|
|
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
|

|
|
13
10
|
|
|
14
11
|
[](https://github.com/teles/new-branch/actions/workflows/ci.yml)
|
|
15
12
|
[](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 "
|
|
50
|
-
--id
|
|
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
|
-
|
|
42
|
+
✅ Branch created and switched to: feat/add-login-page-PROJ-123
|
|
80
43
|
```
|
|
81
44
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
67
|
+
## Documentation
|
|
237
68
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
* @
|
|
2
|
+
* @module config/loadConfig
|
|
3
|
+
*
|
|
3
4
|
* Aggregates configuration sources without merging.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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() {
|