new-branch 0.4.0 → 0.6.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 +71 -17
- package/dist/cli.js +50 -8
- package/dist/config/loadConfig.js +31 -0
- package/dist/config/sources/git.loader.js +39 -0
- package/dist/config/sources/packageJson.loader.js +37 -0
- package/dist/config/sources/rc.loader.js +26 -0
- package/dist/config/types.js +8 -0
- package/dist/config/validate.js +70 -0
- package/dist/git/gitBuiltins.js +79 -0
- package/dist/git/gitConfig.js +2 -2
- package/dist/parseArgs.js +7 -4
- package/dist/runtime/resolveMissingValues.js +8 -2
- package/package.json +4 -6
- package/dist/config/loadProjectConfig.js +0 -19
package/README.md
CHANGED
|
@@ -102,6 +102,35 @@ Example:
|
|
|
102
102
|
- `date` → YYYY-MM-DD
|
|
103
103
|
- `dateCompact` → YYYYMMDD
|
|
104
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
|
+
|
|
105
134
|
---
|
|
106
135
|
|
|
107
136
|
## Built-in Transforms
|
|
@@ -148,31 +177,63 @@ Disable prompts with:
|
|
|
148
177
|
|
|
149
178
|
---
|
|
150
179
|
|
|
151
|
-
## Project Configuration
|
|
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.
|
|
152
191
|
|
|
153
|
-
|
|
192
|
+
Examples
|
|
193
|
+
|
|
194
|
+
1. `.newbranchrc.json` (preferred when present and non-empty):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"pattern": "{type}/{title:slugify}-{id}",
|
|
199
|
+
"types": [
|
|
200
|
+
{ "value": "feat", "label": "Feature" },
|
|
201
|
+
{ "value": "fix", "label": "Fix" }
|
|
202
|
+
],
|
|
203
|
+
"defaultType": "feat"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
2. `package.json` fallback:
|
|
154
208
|
|
|
155
209
|
```json
|
|
156
210
|
{
|
|
157
211
|
"new-branch": {
|
|
158
|
-
"pattern": "{type}/{title:slugify}-{id}"
|
|
212
|
+
"pattern": "{type}/{title:slugify}-{id}",
|
|
213
|
+
"defaultType": "fix"
|
|
159
214
|
}
|
|
160
215
|
}
|
|
161
216
|
```
|
|
162
217
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
You can also define a default pattern using Git config:
|
|
218
|
+
3. Git config fallback (local takes precedence over global):
|
|
166
219
|
|
|
167
220
|
```bash
|
|
168
221
|
git config --local new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
222
|
+
git config --global new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
169
223
|
```
|
|
170
224
|
|
|
171
|
-
|
|
225
|
+
Notes about `type` and `defaultType`
|
|
172
226
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
233
|
+
|
|
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.
|
|
235
|
+
|
|
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.
|
|
176
237
|
|
|
177
238
|
To remove the pattern from Git config:
|
|
178
239
|
|
|
@@ -184,13 +245,6 @@ git config --unset --local new-branch.pattern
|
|
|
184
245
|
git config --unset --global new-branch.pattern
|
|
185
246
|
```
|
|
186
247
|
|
|
187
|
-
When using Git config, the resolution order becomes:
|
|
188
|
-
|
|
189
|
-
1. CLI flags
|
|
190
|
-
2. `package.json` configuration
|
|
191
|
-
3. Git config (`new-branch.pattern`)
|
|
192
|
-
4. Interactive prompt (if enabled)
|
|
193
|
-
|
|
194
248
|
---
|
|
195
249
|
|
|
196
250
|
## Git Safety
|
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,9 @@ 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
7
|
import { getBuiltinValues } from "./runtime/builtins.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadConfig } from "./config/loadConfig.js";
|
|
9
9
|
import { getGitConfig } from "./git/gitConfig.js";
|
|
10
|
+
import { extractGitBuiltinKeysFromPattern, getGitBuiltins, patternNeedsGitBuiltins, } from "./git/gitBuiltins.js";
|
|
10
11
|
import { sanitizeGitRef } from "./git/sanitizeGitRef.js";
|
|
11
12
|
import { validateBranchName } from "./git/validateBranchName.js";
|
|
12
13
|
import { createBranch } from "./git/createBranch.js";
|
|
@@ -49,13 +50,27 @@ function toInitialValues(args) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
export async function run() {
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
53
|
+
// Normalize argv so it works consistently across:
|
|
54
|
+
// - node dist/cli.js --id 123
|
|
55
|
+
// - pnpm dev -- --id 123
|
|
56
|
+
// - tsx src/cli.ts --id 123
|
|
57
|
+
//
|
|
58
|
+
// Notes:
|
|
59
|
+
// - `pnpm` may inject a standalone "--" before the script flags.
|
|
60
|
+
// - `tsx` puts the script path (e.g. `src/cli.ts`) as the first item in `process.argv.slice(2)`.
|
|
61
|
+
// That script path is not a flag, so we strip leading non-flag arguments.
|
|
62
|
+
let argv = process.argv.slice(2);
|
|
63
|
+
// Strip leading positional entries like `src/cli.ts` (common when running via `tsx`).
|
|
64
|
+
while (argv.length > 0 && argv[0] !== "--" && !argv[0].startsWith("-")) {
|
|
65
|
+
argv = argv.slice(1);
|
|
66
|
+
}
|
|
67
|
+
// Strip standalone "--" injected by pnpm.
|
|
68
|
+
if (argv[0] === "--") {
|
|
69
|
+
argv = argv.slice(1);
|
|
70
|
+
}
|
|
57
71
|
const wantsHelp = argv.includes("--help") || argv.includes("-h");
|
|
58
|
-
|
|
72
|
+
// Important: parseArgs should receive the reconstructed argv
|
|
73
|
+
const args = parseArgs(["node", "cli", ...argv]);
|
|
59
74
|
if (wantsHelp) {
|
|
60
75
|
return;
|
|
61
76
|
}
|
|
@@ -63,7 +78,7 @@ export async function run() {
|
|
|
63
78
|
const create = args.options.create === true;
|
|
64
79
|
const prompt = args.options.prompt !== false;
|
|
65
80
|
// Pipeline: pattern -> AST -> resolve values -> render -> sanitize -> validate -> (optional) git -> output
|
|
66
|
-
const projectConfig = await
|
|
81
|
+
const projectConfig = await loadConfig();
|
|
67
82
|
// Git config (respects local -> global precedence automatically)
|
|
68
83
|
let gitPattern;
|
|
69
84
|
if (!args.options.pattern && !projectConfig.pattern) {
|
|
@@ -76,13 +91,40 @@ export async function run() {
|
|
|
76
91
|
const astRes = safe(() => parsePattern(patternRes.value));
|
|
77
92
|
if (!isOk(astRes))
|
|
78
93
|
fail("Invalid pattern.", astRes.error);
|
|
94
|
+
// Runtime built-ins (date, etc.)
|
|
79
95
|
const builtinValues = getBuiltinValues();
|
|
96
|
+
// Git built-ins (only if the pattern references them)
|
|
97
|
+
let gitValues = {};
|
|
98
|
+
if (patternNeedsGitBuiltins(patternRes.value)) {
|
|
99
|
+
const gitKeys = extractGitBuiltinKeysFromPattern(patternRes.value);
|
|
100
|
+
const gitRes = await safeAsync(() => getGitBuiltins(gitKeys));
|
|
101
|
+
if (!isOk(gitRes))
|
|
102
|
+
fail("Failed to resolve git builtins.", gitRes.error);
|
|
103
|
+
// GitBuiltins is compatible with RenderValues (string | undefined)
|
|
104
|
+
gitValues = gitRes.value;
|
|
105
|
+
}
|
|
106
|
+
// Resolve `type` from CLI or config, honoring precedence:
|
|
107
|
+
// 1. CLI --type overrides everything
|
|
108
|
+
// 2. projectConfig.defaultType (if present)
|
|
109
|
+
// 3. if only one type is declared, use that as a convenience
|
|
110
|
+
// 4. otherwise leave undefined so resolveMissingValues will prompt (if prompt===true)
|
|
111
|
+
let resolvedType = args.options.type ?? projectConfig.defaultType;
|
|
112
|
+
if (!resolvedType && projectConfig.types?.length === 1) {
|
|
113
|
+
resolvedType = projectConfig.types[0].value;
|
|
114
|
+
}
|
|
80
115
|
const initialValues = {
|
|
81
116
|
...builtinValues,
|
|
117
|
+
...gitValues,
|
|
82
118
|
...toInitialValues(args),
|
|
119
|
+
type: resolvedType,
|
|
83
120
|
};
|
|
84
121
|
const valuesRes = await safeAsync(() => resolveMissingValues(astRes.value, initialValues, {
|
|
85
122
|
prompt,
|
|
123
|
+
// If project config defines `types`, expose them as choices for the
|
|
124
|
+
// interactive `type` select so the user sees and can choose project values.
|
|
125
|
+
typeChoices: projectConfig.types
|
|
126
|
+
? projectConfig.types.map((t) => ({ name: t.label, value: t.value }))
|
|
127
|
+
: undefined,
|
|
86
128
|
}));
|
|
87
129
|
if (!isOk(valuesRes))
|
|
88
130
|
fail("Failed to resolve required values.", valuesRes.error);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Aggregates configuration sources without merging.
|
|
4
|
+
*
|
|
5
|
+
* Precedence:
|
|
6
|
+
* 1) .new-branchrc.json
|
|
7
|
+
* 2) package.json
|
|
8
|
+
* 3) git config
|
|
9
|
+
*/
|
|
10
|
+
import { rcLoader } from "./sources/rc.loader.js";
|
|
11
|
+
import { packageJsonLoader } from "./sources/packageJson.loader.js";
|
|
12
|
+
import { gitLoader } from "./sources/git.loader.js";
|
|
13
|
+
/**
|
|
14
|
+
* Loads the first configuration found.
|
|
15
|
+
* No merging is performed.
|
|
16
|
+
*/
|
|
17
|
+
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.
|
|
21
|
+
const rcRes = await rcLoader.load();
|
|
22
|
+
if (rcRes.found && rcRes.config && Object.keys(rcRes.config).length > 0)
|
|
23
|
+
return rcRes.config;
|
|
24
|
+
const pkgRes = await packageJsonLoader.load();
|
|
25
|
+
if (pkgRes.found && pkgRes.config && Object.keys(pkgRes.config).length > 0)
|
|
26
|
+
return pkgRes.config;
|
|
27
|
+
const gitRes = await gitLoader.load();
|
|
28
|
+
if (gitRes.found)
|
|
29
|
+
return gitRes.config ?? {};
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getGitConfig } from "../../git/gitConfig.js";
|
|
2
|
+
import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
|
|
3
|
+
function parseGitTypes(raw) {
|
|
4
|
+
return raw
|
|
5
|
+
.split(",")
|
|
6
|
+
.map((s) => s.trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((entry) => {
|
|
9
|
+
const idx = entry.indexOf(":");
|
|
10
|
+
if (idx === -1) {
|
|
11
|
+
return { value: entry, label: entry };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
value: entry.slice(0, idx).trim(),
|
|
15
|
+
label: entry.slice(idx + 1).trim(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export const gitLoader = {
|
|
20
|
+
source: "git",
|
|
21
|
+
async load() {
|
|
22
|
+
const pattern = await getGitConfig("new-branch.pattern");
|
|
23
|
+
const defaultType = await getGitConfig("new-branch.defaultType");
|
|
24
|
+
const typesRaw = await getGitConfig("new-branch.types");
|
|
25
|
+
if (!pattern && !defaultType && !typesRaw) {
|
|
26
|
+
return { found: false, source: "git", config: undefined };
|
|
27
|
+
}
|
|
28
|
+
const cfg = {};
|
|
29
|
+
if (pattern)
|
|
30
|
+
cfg.pattern = pattern;
|
|
31
|
+
if (defaultType)
|
|
32
|
+
cfg.defaultType = defaultType;
|
|
33
|
+
if (typesRaw)
|
|
34
|
+
cfg.types = parseGitTypes(typesRaw);
|
|
35
|
+
const sourceValidated = validateProjectConfigSource(cfg, "git config");
|
|
36
|
+
const finalValidated = validateProjectConfigFinal(sourceValidated, "git config");
|
|
37
|
+
return { found: true, source: "git", config: finalValidated };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
|
|
4
|
+
function isNodeFsError(e) {
|
|
5
|
+
return typeof e === "object" && e !== null && "code" in e;
|
|
6
|
+
}
|
|
7
|
+
export const packageJsonLoader = {
|
|
8
|
+
source: "package.json",
|
|
9
|
+
async load() {
|
|
10
|
+
try {
|
|
11
|
+
const path = join(process.cwd(), "package.json");
|
|
12
|
+
const raw = await readFile(path, "utf8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
15
|
+
return { found: false, source: "package.json", config: undefined };
|
|
16
|
+
}
|
|
17
|
+
const pkg = parsed;
|
|
18
|
+
const block = pkg["new-branch"];
|
|
19
|
+
if (!block) {
|
|
20
|
+
return { found: false, source: "package.json", config: undefined };
|
|
21
|
+
}
|
|
22
|
+
const sourceValidated = validateProjectConfigSource(block, "package.json");
|
|
23
|
+
const finalValidated = validateProjectConfigFinal(sourceValidated, "package.json");
|
|
24
|
+
return {
|
|
25
|
+
found: true,
|
|
26
|
+
source: "package.json",
|
|
27
|
+
config: finalValidated,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
if (isNodeFsError(e) && e.code === "ENOENT") {
|
|
32
|
+
return { found: false, source: "package.json", config: undefined };
|
|
33
|
+
}
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
|
|
4
|
+
export const RC_FILENAME = ".newbranchrc.json";
|
|
5
|
+
function isNodeFsError(e) {
|
|
6
|
+
return typeof e === "object" && e !== null && "code" in e;
|
|
7
|
+
}
|
|
8
|
+
export const rcLoader = {
|
|
9
|
+
source: "rc",
|
|
10
|
+
async load() {
|
|
11
|
+
try {
|
|
12
|
+
const path = join(process.cwd(), RC_FILENAME);
|
|
13
|
+
const raw = await readFile(path, "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
const sourceValidated = validateProjectConfigSource(parsed, RC_FILENAME);
|
|
16
|
+
const finalValidated = validateProjectConfigFinal(sourceValidated, RC_FILENAME);
|
|
17
|
+
return { found: true, source: "rc", config: finalValidated };
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (isNodeFsError(e) && e.code === "ENOENT") {
|
|
21
|
+
return { found: false, source: "rc", config: undefined };
|
|
22
|
+
}
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Validation + normalization logic for `new-branch` configuration.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* 1) validateProjectConfigSource → structural validation per source
|
|
7
|
+
* 2) validateProjectConfigFinal → cross-field business rules
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Throws a standardized configuration error.
|
|
11
|
+
*/
|
|
12
|
+
function invariant(condition, source, message) {
|
|
13
|
+
if (!condition) {
|
|
14
|
+
throw new Error(`Invalid new-branch config from ${source}: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isObject(v) {
|
|
18
|
+
return typeof v === "object" && v !== null;
|
|
19
|
+
}
|
|
20
|
+
function isString(v) {
|
|
21
|
+
return typeof v === "string";
|
|
22
|
+
}
|
|
23
|
+
function trimOrUndefined(v) {
|
|
24
|
+
const t = v.trim();
|
|
25
|
+
return t.length > 0 ? t : undefined;
|
|
26
|
+
}
|
|
27
|
+
function normalizeBranchType(raw, source) {
|
|
28
|
+
invariant(isObject(raw), source, "types[] must be an object");
|
|
29
|
+
const obj = raw;
|
|
30
|
+
const value = trimOrUndefined(String(obj.value ?? ""));
|
|
31
|
+
const label = trimOrUndefined(String(obj.label ?? ""));
|
|
32
|
+
invariant(value, source, "types[].value cannot be empty");
|
|
33
|
+
invariant(label, source, "types[].label cannot be empty");
|
|
34
|
+
return { value, label };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Structural validation for a single source.
|
|
38
|
+
*/
|
|
39
|
+
export function validateProjectConfigSource(raw, source) {
|
|
40
|
+
invariant(isObject(raw), source, "config must be an object");
|
|
41
|
+
const obj = raw;
|
|
42
|
+
const cfg = {};
|
|
43
|
+
if ("pattern" in obj) {
|
|
44
|
+
invariant(isString(obj.pattern), source, "pattern must be a string");
|
|
45
|
+
cfg.pattern = trimOrUndefined(obj.pattern);
|
|
46
|
+
}
|
|
47
|
+
if ("defaultType" in obj) {
|
|
48
|
+
invariant(isString(obj.defaultType), source, "defaultType must be a string");
|
|
49
|
+
cfg.defaultType = trimOrUndefined(obj.defaultType);
|
|
50
|
+
}
|
|
51
|
+
if ("types" in obj) {
|
|
52
|
+
const typesVal = obj.types;
|
|
53
|
+
invariant(Array.isArray(typesVal), source, "types must be an array");
|
|
54
|
+
cfg.types = typesVal.map((t) => normalizeBranchType(t, source));
|
|
55
|
+
}
|
|
56
|
+
return cfg;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Final cross-field validation.
|
|
60
|
+
*/
|
|
61
|
+
export function validateProjectConfigFinal(cfg, source) {
|
|
62
|
+
if (cfg.types) {
|
|
63
|
+
invariant(cfg.types.length > 0, source, "types cannot be empty");
|
|
64
|
+
}
|
|
65
|
+
if (cfg.defaultType && cfg.types) {
|
|
66
|
+
const exists = cfg.types.some((t) => t.value === cfg.defaultType);
|
|
67
|
+
invariant(exists, source, `defaultType "${cfg.defaultType}" must exist in types`);
|
|
68
|
+
}
|
|
69
|
+
return cfg;
|
|
70
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getGitConfig } from "../git/gitConfig.js";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
export const GIT_BUILTIN_KEYS = [
|
|
4
|
+
"shortSha",
|
|
5
|
+
"currentBranch",
|
|
6
|
+
"userName",
|
|
7
|
+
"repoName",
|
|
8
|
+
"lastTag",
|
|
9
|
+
];
|
|
10
|
+
function uniqueKeys(keys) {
|
|
11
|
+
return Array.from(new Set(keys));
|
|
12
|
+
}
|
|
13
|
+
function pickAllKeysIfUndefined(keys) {
|
|
14
|
+
return keys?.length ? uniqueKeys(keys) : GIT_BUILTIN_KEYS;
|
|
15
|
+
}
|
|
16
|
+
async function safeExec(args) {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = (await execa("git", args));
|
|
19
|
+
const value = String(stdout ?? "").trim();
|
|
20
|
+
return value.length ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function deriveRepoName(repoRoot) {
|
|
27
|
+
const parts = repoRoot.split(/[\\/]/).filter(Boolean);
|
|
28
|
+
return parts.length ? parts[parts.length - 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
const RESOLVERS = {
|
|
31
|
+
shortSha: () => safeExec(["rev-parse", "--short", "HEAD"]),
|
|
32
|
+
currentBranch: async () => {
|
|
33
|
+
const value = await safeExec(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
34
|
+
return value === "HEAD" ? undefined : value;
|
|
35
|
+
},
|
|
36
|
+
repoName: async () => {
|
|
37
|
+
const repoRoot = await safeExec(["rev-parse", "--show-toplevel"]);
|
|
38
|
+
return repoRoot ? deriveRepoName(repoRoot) : undefined;
|
|
39
|
+
},
|
|
40
|
+
lastTag: () => safeExec(["describe", "--tags", "--abbrev=0"]),
|
|
41
|
+
userName: async () => {
|
|
42
|
+
const value = await getGitConfig("user.name");
|
|
43
|
+
return value ?? process.env.USER ?? process.env.USERNAME ?? undefined;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
async function resolveGitBuiltins(keys) {
|
|
47
|
+
const wanted = pickAllKeysIfUndefined(keys);
|
|
48
|
+
const entries = await Promise.all(wanted.map(async (key) => {
|
|
49
|
+
const value = await RESOLVERS[key]();
|
|
50
|
+
return [key, value];
|
|
51
|
+
}));
|
|
52
|
+
return entries.reduce((acc, [key, value]) => {
|
|
53
|
+
acc[key] = value;
|
|
54
|
+
return acc;
|
|
55
|
+
}, {});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves git-based built-in variables.
|
|
59
|
+
*
|
|
60
|
+
* - If `keys` is omitted, resolves all supported git builtins.
|
|
61
|
+
* - If `keys` is provided, resolves only those keys.
|
|
62
|
+
*/
|
|
63
|
+
export async function getGitBuiltins(keys) {
|
|
64
|
+
return resolveGitBuiltins(keys);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the pattern contains at least one git builtin key.
|
|
68
|
+
* (Call this before `getGitBuiltins()` if you want to avoid running git at all.)
|
|
69
|
+
*/
|
|
70
|
+
export function patternNeedsGitBuiltins(pattern) {
|
|
71
|
+
return GIT_BUILTIN_KEYS.some((key) => pattern.includes(key));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extracts which git builtin keys are present in the pattern.
|
|
75
|
+
* (Use the returned keys to resolve only what you need.)
|
|
76
|
+
*/
|
|
77
|
+
export function extractGitBuiltinKeysFromPattern(pattern) {
|
|
78
|
+
return GIT_BUILTIN_KEYS.filter((key) => pattern.includes(key));
|
|
79
|
+
}
|
package/dist/git/gitConfig.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
export async function getGitConfig(key) {
|
|
3
3
|
try {
|
|
4
|
-
const { stdout } = await execa("git", ["config", "--get", key]);
|
|
5
|
-
const value = stdout.trim();
|
|
4
|
+
const { stdout } = (await execa("git", ["config", "--get", key]));
|
|
5
|
+
const value = String(stdout ?? "").trim();
|
|
6
6
|
return value.length ? value : undefined;
|
|
7
7
|
}
|
|
8
8
|
catch {
|
package/dist/parseArgs.js
CHANGED
|
@@ -21,11 +21,14 @@ export function parseArgs(argv = process.argv) {
|
|
|
21
21
|
const parsed = cli.parse(cleaned);
|
|
22
22
|
const opts = parsed.options;
|
|
23
23
|
const options = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Accept numeric or string-like values and coerce to string when present.
|
|
25
|
+
pattern: opts.pattern !== undefined ? String(opts.pattern) : undefined,
|
|
26
|
+
id: opts.id !== undefined ? String(opts.id) : undefined,
|
|
27
|
+
title: opts.title !== undefined ? String(opts.title) : undefined,
|
|
28
|
+
type: opts.type !== undefined ? String(opts.type) : undefined,
|
|
28
29
|
create: typeof opts.create === "boolean" ? opts.create : undefined,
|
|
30
|
+
// CAC provides `--no-prompt` as `noPrompt` normally, but the flag will
|
|
31
|
+
// also be available as `prompt` when parsed; keep boolean handling.
|
|
29
32
|
prompt: typeof opts.prompt === "boolean" ? opts.prompt : undefined,
|
|
30
33
|
quiet: typeof opts.quiet === "boolean" ? opts.quiet : undefined,
|
|
31
34
|
help: typeof opts.help === "boolean" ? opts.help : undefined,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { input, select } from "@inquirer/prompts";
|
|
2
1
|
import { TYPE_CHOICES } from "../runtime/enums.js";
|
|
3
2
|
/**
|
|
4
3
|
* Resolves missing variable values required by a parsed pattern.
|
|
@@ -40,10 +39,17 @@ export async function resolveMissingValues(parsed, initialValues, opts) {
|
|
|
40
39
|
if (!opts.prompt) {
|
|
41
40
|
throw new Error(`Missing required value: "${name}"`);
|
|
42
41
|
}
|
|
42
|
+
// Lazily import the interactive prompts to avoid importing
|
|
43
|
+
// `@inquirer/prompts` at module load time. This prevents environments
|
|
44
|
+
// with incompatible Node.js versions from failing when the CLI is
|
|
45
|
+
// executed in non-interactive mode (e.g., `--no-prompt`).
|
|
46
|
+
const prompts = await import("@inquirer/prompts");
|
|
47
|
+
const { input, select } = prompts;
|
|
43
48
|
if (name === "type") {
|
|
49
|
+
const choices = opts.typeChoices ?? TYPE_CHOICES;
|
|
44
50
|
const selected = await select({
|
|
45
51
|
message: "Select branch type:",
|
|
46
|
-
choices
|
|
52
|
+
choices,
|
|
47
53
|
});
|
|
48
54
|
values[name] = selected;
|
|
49
55
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "new-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Generate and create standardized git branch names from a pattern.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"lint": "eslint src --ext .ts"
|
|
27
27
|
},
|
|
28
28
|
"bin": {
|
|
29
|
-
"new-branch": "
|
|
30
|
-
"git-
|
|
29
|
+
"new-branch": "dist/cli.js",
|
|
30
|
+
"git-new-branch": "dist/cli.js",
|
|
31
|
+
"git-nb": "dist/cli.js"
|
|
31
32
|
},
|
|
32
33
|
"author": "Teles <github.com/teles>",
|
|
33
34
|
"license": "MIT",
|
|
@@ -41,9 +42,6 @@
|
|
|
41
42
|
"bugs": {
|
|
42
43
|
"url": "https://github.com/teles/new-branch/issues"
|
|
43
44
|
},
|
|
44
|
-
"new-branch": {
|
|
45
|
-
"pattern": "{type}/{title:lower}-{id}"
|
|
46
|
-
},
|
|
47
45
|
"homepage": "https://github.com/teles/new-branch#readme",
|
|
48
46
|
"packageManager": "pnpm@10.22.0",
|
|
49
47
|
"devDependencies": {
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
export async function loadProjectConfig() {
|
|
4
|
-
try {
|
|
5
|
-
const path = join(process.cwd(), "package.json");
|
|
6
|
-
const raw = await readFile(path, "utf8");
|
|
7
|
-
const pkg = JSON.parse(raw);
|
|
8
|
-
const config = pkg["new-branch"];
|
|
9
|
-
if (!config || typeof config !== "object")
|
|
10
|
-
return {};
|
|
11
|
-
const cfg = config;
|
|
12
|
-
return {
|
|
13
|
-
pattern: typeof cfg.pattern === "string" ? cfg.pattern : undefined,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
}
|