new-branch 0.7.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 +92 -2
  3. package/dist/config/loadConfig.js +17 -7
  4. package/dist/config/sources/git.loader.js +27 -0
  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 +61 -8
  9. package/dist/didactic/explain.js +29 -2
  10. package/dist/didactic/listTransforms.js +7 -3
  11. package/dist/didactic/printConfig.js +8 -3
  12. package/dist/git/createBranch.js +10 -3
  13. package/dist/git/gitBuiltins.js +73 -6
  14. package/dist/git/gitConfig.js +28 -4
  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 +34 -0
  19. package/dist/pattern/parsePattern.js +14 -0
  20. package/dist/pattern/transforms/after.js +15 -0
  21. package/dist/pattern/transforms/before.js +15 -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 +15 -0
  25. package/dist/pattern/transforms/index.js +10 -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 +13 -0
  31. package/dist/pattern/transforms/renderPattern.js +9 -0
  32. package/dist/pattern/transforms/replace.js +13 -0
  33. package/dist/pattern/transforms/replaceAll.js +13 -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 +15 -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
@@ -1,29 +1,61 @@
1
1
  /**
2
- * @fileoverview
3
- * Validation + normalization logic for `new-branch` configuration.
2
+ * @module config/validate
4
3
  *
5
- * Strategy:
6
- * 1) validateProjectConfigSource → structural validation per source
7
- * 2) validateProjectConfigFinal → cross-field business rules
4
+ * Validation and normalisation logic for `new-branch` configuration.
5
+ *
6
+ * @remarks
7
+ * The validation pipeline consists of two stages:
8
+ * 1. {@link validateProjectConfigSource} — structural validation per source.
9
+ * 2. {@link validateProjectConfigFinal} — cross-field business rules.
8
10
  */
9
11
  /**
10
- * Throws a standardized configuration error.
12
+ * Throws a standardised configuration error when `condition` is falsy.
13
+ *
14
+ * @param condition - The value to assert.
15
+ * @param source - Label identifying the config source (for error messages).
16
+ * @param message - Human-readable description of the violated rule.
11
17
  */
12
18
  function invariant(condition, source, message) {
13
19
  if (!condition) {
14
20
  throw new Error(`Invalid new-branch config from ${source}: ${message}`);
15
21
  }
16
22
  }
23
+ /**
24
+ * Checks whether `v` is a non-null object.
25
+ *
26
+ * @param v - The value to check.
27
+ * @returns `true` if `v` is an object and not `null`.
28
+ */
17
29
  function isObject(v) {
18
30
  return typeof v === "object" && v !== null;
19
31
  }
32
+ /**
33
+ * Type guard for strings.
34
+ *
35
+ * @param v - The value to check.
36
+ * @returns `true` if `v` is a `string`.
37
+ */
20
38
  function isString(v) {
21
39
  return typeof v === "string";
22
40
  }
41
+ /**
42
+ * Trims a string and returns `undefined` when the result is empty.
43
+ *
44
+ * @param v - The string to trim.
45
+ * @returns The trimmed string, or `undefined` if blank.
46
+ */
23
47
  function trimOrUndefined(v) {
24
48
  const t = v.trim();
25
49
  return t.length > 0 ? t : undefined;
26
50
  }
51
+ /**
52
+ * Normalises a raw branch-type entry into a validated {@link BranchType}.
53
+ *
54
+ * @param raw - The unknown value to normalise.
55
+ * @param source - Config source label (for error messages).
56
+ * @returns A valid {@link BranchType}.
57
+ * @throws {@link Error} If `value` or `label` is missing/empty.
58
+ */
27
59
  function normalizeBranchType(raw, source) {
28
60
  invariant(isObject(raw), source, "types[] must be an object");
29
61
  const obj = raw;
@@ -34,7 +66,17 @@ function normalizeBranchType(raw, source) {
34
66
  return { value, label };
35
67
  }
36
68
  /**
37
- * Structural validation for a single source.
69
+ * Performs structural validation for a single configuration source.
70
+ *
71
+ * @remarks
72
+ * Validates shapes but does **not** check cross-field rules
73
+ * (e.g. `defaultType` existing in `types`). Use
74
+ * {@link validateProjectConfigFinal} for that.
75
+ *
76
+ * @param raw - The raw config object to validate.
77
+ * @param source - Config source label (for error messages).
78
+ * @returns A validated {@link ProjectConfig}.
79
+ * @throws {@link Error} On any structural violation.
38
80
  */
39
81
  export function validateProjectConfigSource(raw, source) {
40
82
  invariant(isObject(raw), source, "config must be an object");
@@ -71,7 +113,18 @@ export function validateProjectConfigSource(raw, source) {
71
113
  return cfg;
72
114
  }
73
115
  /**
74
- * Final cross-field validation.
116
+ * Performs cross-field (semantic) validation on an already-structurally-valid config.
117
+ *
118
+ * @remarks
119
+ * Rules enforced:
120
+ * - `types`, when present, must not be empty.
121
+ * - `defaultType`, when present alongside `types`, must match one of
122
+ * the declared type values.
123
+ *
124
+ * @param cfg - The structurally-valid config to check.
125
+ * @param source - Config source label (for error messages).
126
+ * @returns The same config if all rules pass.
127
+ * @throws {@link Error} On any semantic violation.
75
128
  */
76
129
  export function validateProjectConfigFinal(cfg, source) {
77
130
  if (cfg.types) {
@@ -1,8 +1,24 @@
1
1
  /**
2
- * Produces a structured breakdown of the full branch-name pipeline.
2
+ * Produces a structured, human-readable breakdown of the full
3
+ * branch-name pipeline.
3
4
  *
5
+ * @remarks
4
6
  * No branch is created — this is purely informational.
5
7
  * Used by the `--explain` CLI flag.
8
+ *
9
+ * Sections printed:
10
+ * 1. Pattern and source
11
+ * 2. Variables used
12
+ * 3. Builtin values
13
+ * 4. Git values
14
+ * 5. CLI values
15
+ * 6. Transform chain with intermediate values
16
+ * 7. Rendered / Sanitized output
17
+ * 8. Max-length truncation (when applicable)
18
+ * 9. Final branch name
19
+ *
20
+ * @param input - The collected pipeline data.
21
+ * @returns A multi-line string ready to be printed to stdout.
6
22
  */
7
23
  export function explain(input) {
8
24
  const lines = [];
@@ -62,6 +78,17 @@ export function explain(input) {
62
78
  if (input.rendered !== input.sanitized) {
63
79
  lines.push(` Sanitized: ${input.sanitized}`);
64
80
  }
65
- lines.push(`\n Final branch: ${input.sanitized}`);
81
+ // 8. Max-length truncation
82
+ if (input.maxLength !== undefined) {
83
+ lines.push(` Max length: ${input.maxLength}`);
84
+ if (input.truncated !== undefined && input.truncated !== input.sanitized) {
85
+ lines.push(` Truncated: ${input.truncated}`);
86
+ }
87
+ else {
88
+ lines.push(` Truncated: (no truncation needed)`);
89
+ }
90
+ }
91
+ const finalName = input.truncated ?? input.sanitized;
92
+ lines.push(`\n Final branch: ${finalName}`);
66
93
  return lines.join("\n");
67
94
  }
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Prints a formatted table of all available transforms.
2
+ * Formats a human-readable table of all available transforms.
3
3
  *
4
- * Each transform is listed with its name, summary and usage example.
5
- * This is used by the `--list-transforms` CLI flag.
4
+ * @remarks
5
+ * Each transform is listed with its name, summary, and an optional
6
+ * usage example. Used by the `--list-transforms` CLI flag.
7
+ *
8
+ * @param transforms - The ordered list of {@link TransformDef} definitions.
9
+ * @returns A multi-line formatted string ready to be printed to stdout.
6
10
  */
7
11
  export function listTransforms(transforms) {
8
12
  const lines = [];
@@ -1,8 +1,13 @@
1
1
  /**
2
- * Formats the resolved configuration for display.
2
+ * Formats the resolved project configuration for display.
3
3
  *
4
- * Shows the final resolved values from whichever source took precedence.
5
- * This is used by the `--print-config` CLI flag.
4
+ * @remarks
5
+ * Shows the final resolved values from whichever source took
6
+ * precedence. Used by the `--print-config` CLI flag.
7
+ *
8
+ * @param config - The resolved {@link ProjectConfig}.
9
+ * @param source - Human-readable label identifying the winning source.
10
+ * @returns A multi-line formatted string ready to be printed to stdout.
6
11
  */
7
12
  export function printConfig(config, source) {
8
13
  const lines = [];
@@ -2,11 +2,18 @@ import { execa } from "execa";
2
2
  /**
3
3
  * Creates and switches to a new Git branch.
4
4
  *
5
- * Strategy:
5
+ * @remarks
6
+ * Uses a two-step strategy for maximum compatibility:
6
7
  * 1. Try `git switch -c <name>` (modern Git ≥ 2.23).
7
- * 2. Fallback to `git checkout -b <name>` if switch is unavailable.
8
+ * 2. Fallback to `git checkout -b <name>` if `switch` is unavailable.
8
9
  *
9
- * @throws Error if branch creation fails.
10
+ * @param name - The name of the branch to create.
11
+ * @throws {@link Error} If both `git switch -c` and `git checkout -b` fail.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * await createBranch("feat/my-feature");
16
+ * ```
10
17
  */
11
18
  export async function createBranch(name) {
12
19
  try {
@@ -1,5 +1,8 @@
1
1
  import { getGitConfig } from "../git/gitConfig.js";
2
2
  import { execa } from "execa";
3
+ /**
4
+ * Ordered list of all supported {@link GitBuiltinKey} values.
5
+ */
3
6
  export const GIT_BUILTIN_KEYS = [
4
7
  "shortSha",
5
8
  "currentBranch",
@@ -7,12 +10,30 @@ export const GIT_BUILTIN_KEYS = [
7
10
  "repoName",
8
11
  "lastTag",
9
12
  ];
13
+ /**
14
+ * Deduplicates an array of {@link GitBuiltinKey} values.
15
+ *
16
+ * @param keys - Array of keys that may contain duplicates.
17
+ * @returns A new array with unique keys only.
18
+ */
10
19
  function uniqueKeys(keys) {
11
20
  return Array.from(new Set(keys));
12
21
  }
22
+ /**
23
+ * Returns the requested keys, or all supported keys when none are specified.
24
+ *
25
+ * @param keys - Optional subset of keys to resolve.
26
+ * @returns The keys to resolve (deduplicated).
27
+ */
13
28
  function pickAllKeysIfUndefined(keys) {
14
29
  return keys?.length ? uniqueKeys(keys) : GIT_BUILTIN_KEYS;
15
30
  }
31
+ /**
32
+ * Executes a git command and returns its trimmed stdout, or `undefined` on failure.
33
+ *
34
+ * @param args - Arguments to pass to the `git` command.
35
+ * @returns The trimmed stdout output, or `undefined` if the command fails or produces no output.
36
+ */
16
37
  async function safeExec(args) {
17
38
  try {
18
39
  const { stdout } = (await execa("git", args));
@@ -23,10 +44,23 @@ async function safeExec(args) {
23
44
  return undefined;
24
45
  }
25
46
  }
47
+ /**
48
+ * Derives the repository name from the absolute path to the repo root.
49
+ *
50
+ * @param repoRoot - Absolute path returned by `git rev-parse --show-toplevel`.
51
+ * @returns The last segment of the path, or `undefined` if the path is empty.
52
+ */
26
53
  function deriveRepoName(repoRoot) {
27
54
  const parts = repoRoot.split(/[\\/]/).filter(Boolean);
28
55
  return parts.length ? parts[parts.length - 1] : undefined;
29
56
  }
57
+ /**
58
+ * Resolver map for each supported {@link GitBuiltinKey}.
59
+ *
60
+ * @remarks
61
+ * Each resolver is an async function that returns the resolved value
62
+ * or `undefined` when unavailable.
63
+ */
30
64
  const RESOLVERS = {
31
65
  shortSha: () => safeExec(["rev-parse", "--short", "HEAD"]),
32
66
  currentBranch: async () => {
@@ -43,6 +77,12 @@ const RESOLVERS = {
43
77
  return value ?? process.env.USER ?? process.env.USERNAME ?? undefined;
44
78
  },
45
79
  };
80
+ /**
81
+ * Internal resolver that resolves the requested git builtin keys in parallel.
82
+ *
83
+ * @param keys - Optional subset of keys to resolve. When omitted, all keys are resolved.
84
+ * @returns An object containing the resolved values.
85
+ */
46
86
  async function resolveGitBuiltins(keys) {
47
87
  const wanted = pickAllKeysIfUndefined(keys);
48
88
  const entries = await Promise.all(wanted.map(async (key) => {
@@ -57,22 +97,49 @@ async function resolveGitBuiltins(keys) {
57
97
  /**
58
98
  * Resolves git-based built-in variables.
59
99
  *
60
- * - If `keys` is omitted, resolves all supported git builtins.
61
- * - If `keys` is provided, resolves only those keys.
100
+ * @remarks
101
+ * Each variable maps to a git command executed in the current repository.
102
+ * Resolution is done in parallel for performance.
103
+ *
104
+ * @param keys - Optional subset of {@link GitBuiltinKey} values to resolve.
105
+ * When omitted, all supported git builtins are resolved.
106
+ * @returns A promise resolving to a {@link GitBuiltins} object.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * // Resolve only what is needed
111
+ * const builtins = await getGitBuiltins(["shortSha", "currentBranch"]);
112
+ * builtins.shortSha; // "abc1234"
113
+ * builtins.currentBranch; // "main"
114
+ * ```
62
115
  */
63
116
  export async function getGitBuiltins(keys) {
64
117
  return resolveGitBuiltins(keys);
65
118
  }
66
119
  /**
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.)
120
+ * Checks whether a pattern string references at least one git builtin key.
121
+ *
122
+ * @remarks
123
+ * Use this before calling {@link getGitBuiltins} to avoid spawning
124
+ * unnecessary git subprocesses.
125
+ *
126
+ * @param pattern - The raw pattern string to check.
127
+ * @returns `true` if the pattern contains at least one {@link GitBuiltinKey}.
69
128
  */
70
129
  export function patternNeedsGitBuiltins(pattern) {
71
130
  return GIT_BUILTIN_KEYS.some((key) => pattern.includes(key));
72
131
  }
73
132
  /**
74
- * Extracts which git builtin keys are present in the pattern.
75
- * (Use the returned keys to resolve only what you need.)
133
+ * Extracts which {@link GitBuiltinKey} values are present in a pattern.
134
+ *
135
+ * @param pattern - The raw pattern string to inspect.
136
+ * @returns An array of {@link GitBuiltinKey} values found in the pattern.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * extractGitBuiltinKeysFromPattern("{currentBranch}-{shortSha}");
141
+ * // => ["shortSha", "currentBranch"]
142
+ * ```
76
143
  */
77
144
  export function extractGitBuiltinKeysFromPattern(pattern) {
78
145
  return GIT_BUILTIN_KEYS.filter((key) => pattern.includes(key));
@@ -1,4 +1,19 @@
1
1
  import { execa } from "execa";
2
+ /**
3
+ * Retrieves a single Git config value by key.
4
+ *
5
+ * @remarks
6
+ * Runs `git config --get <key>` as a subprocess. Returns `undefined`
7
+ * when the key is not set or the command fails (e.g. outside a repo).
8
+ *
9
+ * @param key - The Git config key to look up (e.g. `"user.name"`).
10
+ * @returns The trimmed config value, or `undefined` if not found.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const name = await getGitConfig("user.name"); // "Alice" | undefined
15
+ * ```
16
+ */
2
17
  export async function getGitConfig(key) {
3
18
  try {
4
19
  const { stdout } = (await execa("git", ["config", "--get", key]));
@@ -10,11 +25,20 @@ export async function getGitConfig(key) {
10
25
  }
11
26
  }
12
27
  /**
13
- * Returns all git config entries matching a key prefix using `--get-regexp`.
14
- * Each entry is returned as a `[key, value]` tuple.
28
+ * Returns all git config entries matching a key pattern using `--get-regexp`.
29
+ *
30
+ * @remarks
31
+ * Each entry is returned as a `[key, value]` tuple. Returns an empty
32
+ * array if no entries match or if the command fails.
33
+ *
34
+ * @param pattern - A regular expression pattern passed to `git config --get-regexp`.
35
+ * @returns An array of `[key, value]` tuples.
15
36
  *
16
- * Example: `getGitConfigRegexp("new-branch.patterns\\.")` returns
17
- * `[["new-branch.patterns.hotfix", "hotfix/{id}"], ...]`
37
+ * @example
38
+ * ```ts
39
+ * const entries = await getGitConfigRegexp("^new-branch\\.patterns\\.");
40
+ * // => [["new-branch.patterns.hotfix", "hotfix/{id}"], ...]
41
+ * ```
18
42
  */
19
43
  export async function getGitConfigRegexp(pattern) {
20
44
  try {
@@ -1,10 +1,28 @@
1
1
  /**
2
- * Performs a lightweight sanitization before delegating
3
- * full validation to Git.
2
+ * Performs a lightweight sanitization of a Git ref name.
4
3
  *
5
- * This does NOT try to fully reimplement Git rules.
6
- * Final validation must be done via:
7
- * git check-ref-format --branch
4
+ * @remarks
5
+ * This function applies a set of heuristic cleanups to make the input
6
+ * more likely to pass `git check-ref-format --branch`. It does **not**
7
+ * fully reimplement all Git ref rules — final validation must still be
8
+ * delegated to Git itself via {@link validateBranchName}.
9
+ *
10
+ * Sanitization steps:
11
+ * 1. Trim whitespace and replace internal whitespace with `-`.
12
+ * 2. Remove characters forbidden by Git (`~ ^ : ? * [ ] \`).
13
+ * 3. Remove `@{` sequences.
14
+ * 4. Collapse multiple slashes and repeated dots.
15
+ * 5. Strip leading dashes/slashes and trailing slashes/dots.
16
+ * 6. Remove `.lock` suffix.
17
+ *
18
+ * @param input - The raw ref name to sanitize.
19
+ * @returns The sanitized ref name.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * sanitizeGitRef("feat/My Feature!!"); // => "feat/My-Feature"
24
+ * sanitizeGitRef("--leading/slash"); // => "leading/slash"
25
+ * ```
8
26
  */
9
27
  export function sanitizeGitRef(input) {
10
28
  let name = input.trim();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Truncates a string from the end to ensure it does not exceed `maxLength`.
3
+ *
4
+ * @remarks
5
+ * This is intentionally simple and deterministic:
6
+ * - If the string length is `<= maxLength`, return it unchanged.
7
+ * - Otherwise, cut from the end.
8
+ *
9
+ * Used by the `--max-length` CLI option after sanitization and before
10
+ * branch-name validation.
11
+ *
12
+ * @param value - The string to truncate.
13
+ * @param maxLength - The maximum allowed length. Must be a positive integer (`>= 1`).
14
+ * @returns The (possibly truncated) string.
15
+ * @throws {@link Error} If `maxLength` is not a positive integer.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * truncateEnd("feat/my-feature", 10); // => "feat/my-fe"
20
+ * truncateEnd("short", 100); // => "short"
21
+ * ```
22
+ */
23
+ export function truncateEnd(value, maxLength) {
24
+ if (!Number.isInteger(maxLength) || maxLength < 1) {
25
+ throw new Error(`--max-length must be a positive integer (>= 1), got "${maxLength}".`);
26
+ }
27
+ if (value.length <= maxLength) {
28
+ return value;
29
+ }
30
+ return value.slice(0, maxLength);
31
+ }
@@ -2,10 +2,19 @@ import { execa } from "execa";
2
2
  /**
3
3
  * Validates a branch name by delegating to Git itself.
4
4
  *
5
- * Uses:
6
- * git check-ref-format --branch <name>
5
+ * @remarks
6
+ * Invokes `git check-ref-format --branch <name>` as a subprocess.
7
+ * This ensures the name complies with all rules enforced by the
8
+ * installed version of Git.
7
9
  *
8
- * Throws if invalid.
10
+ * @param name - The branch name to validate.
11
+ * @throws {@link Error} If the branch name does not pass `git check-ref-format`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * await validateBranchName("feat/my-branch"); // resolves
16
+ * await validateBranchName(".."); // throws
17
+ * ```
9
18
  */
10
19
  export async function validateBranchName(name) {
11
20
  try {
package/dist/parseArgs.js CHANGED
@@ -1,4 +1,18 @@
1
+ /**
2
+ * @module parseArgs
3
+ *
4
+ * CLI argument parser built on top of {@link https://github.com/cacjs/cac | cac}.
5
+ * Defines every flag accepted by `new-branch` and coerces raw `process.argv`
6
+ * values into a strongly-typed {@link ParsedArgs} object.
7
+ */
1
8
  import { cac } from "cac";
9
+ /**
10
+ * Remove the bare `"--"` separator from an argv array so that `cac` does not
11
+ * choke on it. Everything before and after the separator is preserved.
12
+ *
13
+ * @param argv - Raw argument vector (typically `process.argv`).
14
+ * @returns A new array with the `"--"` element removed, if present.
15
+ */
2
16
  function stripDoubleDash(argv) {
3
17
  const idx = argv.indexOf("--");
4
18
  if (idx === -1)
@@ -6,6 +20,24 @@ function stripDoubleDash(argv) {
6
20
  // keep node + script, remove the "--" separator and retain the rest
7
21
  return [...argv.slice(0, idx), ...argv.slice(idx + 1)];
8
22
  }
23
+ /**
24
+ * Parse a raw argument vector into a typed {@link ParsedArgs} object.
25
+ *
26
+ * @remarks
27
+ * String-like option values are coerced via `String()` and numeric ones via
28
+ * `Number()`, so callers always receive the expected primitive types regardless
29
+ * of how the shell delivers them.
30
+ *
31
+ * @param argv - The argument vector to parse (defaults to `process.argv`).
32
+ * @returns Parsed and coerced CLI arguments.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const { options } = parseArgs(["node", "new-branch", "--type", "feat", "--id", "42"]);
37
+ * // options.type === "feat"
38
+ * // options.id === "42"
39
+ * ```
40
+ */
9
41
  export function parseArgs(argv = process.argv) {
10
42
  const cli = cac("new-branch");
11
43
  cli
@@ -20,6 +52,7 @@ export function parseArgs(argv = process.argv) {
20
52
  .option("--explain", "Show a detailed breakdown of the branch pipeline without creating a branch")
21
53
  .option("--list-transforms", "List all available transforms")
22
54
  .option("--print-config", "Print the resolved configuration")
55
+ .option("-L, --max-length <n>", "Maximum length for the final branch name")
23
56
  .help();
24
57
  const cleaned = stripDoubleDash(argv);
25
58
  const parsed = cli.parse(cleaned);
@@ -40,6 +73,7 @@ export function parseArgs(argv = process.argv) {
40
73
  explain: typeof opts.explain === "boolean" ? opts.explain : undefined,
41
74
  listTransforms: typeof opts.listTransforms === "boolean" ? opts.listTransforms : undefined,
42
75
  printConfig: typeof opts.printConfig === "boolean" ? opts.printConfig : undefined,
76
+ maxLength: opts.maxLength !== undefined ? Number(opts.maxLength) : undefined,
43
77
  };
44
78
  return {
45
79
  options,
@@ -121,6 +121,14 @@ export function parsePattern(input) {
121
121
  variablesUsed: uniquePreserveOrder(variablesUsed),
122
122
  };
123
123
  }
124
+ /**
125
+ * Parses a single transform segment like `"slugify"` or `"max:25"`
126
+ * into a {@link TransformNode}.
127
+ *
128
+ * @param segment - The raw transform string (e.g. `"replace:_:-"`).
129
+ * @returns The parsed {@link TransformNode}.
130
+ * @throws {@link Error} If the segment is empty or has no name.
131
+ */
124
132
  function parseTransform(segment) {
125
133
  // segment examples:
126
134
  // - "slugify"
@@ -134,6 +142,12 @@ function parseTransform(segment) {
134
142
  }
135
143
  return { name, args };
136
144
  }
145
+ /**
146
+ * Deduplicates a string array while preserving the original order.
147
+ *
148
+ * @param items - The array of strings to deduplicate.
149
+ * @returns A new array containing only the first occurrence of each item.
150
+ */
137
151
  function uniquePreserveOrder(items) {
138
152
  const seen = new Set();
139
153
  const out = [];
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Transform: **after**
3
+ *
4
+ * @remarks
5
+ * Appends a suffix to the value. If the value is empty, returns
6
+ * an empty string (the suffix is not added to avoid dangling separators).
7
+ *
8
+ * Pattern syntax: `{variable:after:suffix}`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * after.fn("feat", ["-wip"]); // => "feat-wip"
13
+ * after.fn("", ["-wip"]); // => ""
14
+ * ```
15
+ */
1
16
  export const after = {
2
17
  name: "after",
3
18
  fn: (value, [suffix]) => {
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Transform: **before**
3
+ *
4
+ * @remarks
5
+ * Prepends a prefix to the value. If the value is empty, returns
6
+ * an empty string (the prefix is not added to avoid dangling separators).
7
+ *
8
+ * Pattern syntax: `{variable:before:prefix}`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * before.fn("fix", ["hotfix-"]); // => "hotfix-fix"
13
+ * before.fn("", ["hotfix-"]); // => ""
14
+ * ```
15
+ */
1
16
  export const before = {
2
17
  name: "before",
3
18
  fn: (value, [prefix]) => {
@@ -1,14 +1,20 @@
1
1
  import { splitWords, upperFirst } from "./helpers/words.js";
2
2
  /**
3
- * Transform: camel
3
+ * Transform: **camel**
4
4
  *
5
- * Converts an input string into camelCase. Uses `splitWords` to extract word
6
- * boundaries and lower-cases the words before joining. The first word is kept
7
- * in lower-case; subsequent words are capitalized using `upperFirst`.
5
+ * @remarks
6
+ * Converts an input string into camelCase. Uses {@link splitWords} to
7
+ * extract word boundaries and lower-cases the words before joining.
8
+ * The first word is kept in lower-case; subsequent words are
9
+ * capitalised with {@link upperFirst}.
8
10
  *
9
- * Examples:
10
- * - "My Task" -> "myTask"
11
- * - "HTTP Server" -> "httpServer"
11
+ * Pattern syntax: `{variable:camel}`
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * camel.fn("My Task", []); // => "myTask"
16
+ * camel.fn("HTTP Server", []); // => "httpServer"
17
+ * ```
12
18
  */
13
19
  export const camel = {
14
20
  name: "camel",
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @module helpers/words
3
+ *
4
+ * Word-splitting utilities used by case-conversion transforms
5
+ * (camel, kebab, snake, title, etc.).
6
+ */
1
7
  /**
2
8
  * Splits an input string into word-like segments.
3
9
  *
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Transform: **ifEmpty**
3
+ *
4
+ * @remarks
5
+ * Provides a fallback value when the current value is an empty string.
6
+ * Useful for guaranteeing a non-empty segment in the branch name.
7
+ *
8
+ * Pattern syntax: `{variable:ifEmpty:fallback}`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * ifEmpty.fn("", ["no-title"]); // => "no-title"
13
+ * ifEmpty.fn("hello", ["unused"]); // => "hello"
14
+ * ```
15
+ */
1
16
  export const ifEmpty = {
2
17
  name: "ifEmpty",
3
18
  fn: (value, [fallback]) => {