new-branch 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,299 +1,24 @@
1
- # new-branch CLI --- Specification
2
-
3
- ## 1. Overview
4
-
5
- `new-branch` is a CLI tool for generating standardized Git branch names
6
- based on a configurable pattern and optional interactive prompts.
7
-
8
- It can be executed in two ways:
9
-
10
- ```bash
11
- npx new-branch
12
- git nb
13
- ```
14
-
15
- ---
16
-
17
- ## 2. Goals
18
-
19
- - Standardize branch naming across teams.
20
- - Provide interactive and non-interactive modes.
21
- - Support a composable transformation pipeline (functional style).
22
- - Ensure generated names are always valid Git references.
23
- - Be easily extensible via custom transforms.
24
-
25
- ---
26
-
27
- ## 3. Usage
28
-
29
- ### 3.1 Basic
30
-
31
- ```bash
32
- npx new-branch
33
- ```
34
-
35
- Runs in interactive mode if required fields are missing.
36
-
37
- ---
38
-
39
- ### 3.2 With config file
40
-
41
- ```bash
42
- npx new-branch --config .newbranchrc
43
- ```
44
-
45
- Alias:
46
-
47
- ```bash
48
- npx new-branch -c .newbranchrc
49
- ```
50
-
51
- ---
52
-
53
- ### 3.3 With custom pattern
54
-
55
- ```bash
56
- npx new-branch --pattern "{type}/{title:slugify;max:25}-{id}"
57
- ```
58
-
59
- Alias:
60
-
61
- ```bash
62
- npx new-branch -p "{type}/{title:slugify;max:25}-{id}"
63
- ```
64
-
65
- ---
66
-
67
- ### 3.4 Non-interactive mode
68
-
69
- ```bash
70
- npx new-branch --pattern "{type}/{title:slugify;max:25}-{id}" --id STK-123 --title "My very interesting task" --type feat
71
- ```
72
-
73
- If all required variables are provided, no prompt is shown.
74
-
75
- ---
76
-
77
- ## 4. Configuration Precedence
78
-
79
- Resolution order (highest priority first):
80
-
81
- 1. CLI flags
82
- 2. Environment variables
83
- 3. Config file (.newbranchrc)
84
- 4. Defaults
85
- 5. Interactive prompt (only if required values are missing)
86
-
87
- ---
88
-
89
- ## 5. Pattern Language
90
-
91
- ### 5.1 Syntax
92
-
93
- Pattern example:
94
-
95
- {type}/{title:slugify;max:25}-{id}
96
-
97
- Structure:
98
-
99
- {variable:transform1;transform2;transformWithArg:arg}
100
-
101
- ### 5.2 Parsing Model
102
-
103
- Each token is parsed into:
104
-
105
- - variable name
106
- - ordered list of transforms
107
- - optional arguments per transform
108
-
109
- Example AST representation:
110
-
111
- ```json
112
- {
113
- "variable": "title",
114
- "pipeline": [{ "fn": "slugify" }, { "fn": "max", "args": [25] }]
115
- }
116
- ```
117
-
118
- ---
119
-
120
- ## 6. Built-in Variables
121
-
122
- | Variable | Description |
123
- | -------- | ------------------------------- |
124
- | type | Branch type (feat, fix, etc.) |
125
- | title | Human-readable task title |
126
- | id | Task identifier (e.g., STK-123) |
127
-
128
- ---
129
-
130
- ## 7. Built-in Transforms
131
-
132
- All transforms must be pure functions.
133
-
134
- ### 7.1 String Transforms
135
-
136
- | Transform | Description |
137
- | --------- | ------------------------- |
138
- | slugify | Converts to URL-safe slug |
139
- | lowercase | Converts to lowercase |
140
- | uppercase | Converts to uppercase |
141
- | trim | Trims whitespace |
142
- | titlecase | Capitalizes words |
143
-
144
- ### 7.2 Argument-based Transforms
145
-
146
- | Transform | Description | Example |
147
- | --------- | ------------------------------ | ------- |
148
- | max | Truncates string to max length | max:25 |
149
- | pad | Pads string to length | pad:10 |
150
-
151
- ### 7.3 Validation Transforms
152
-
153
- Validation transforms do not modify a value but throw errors if invalid.
154
-
155
- | Transform | Description |
156
- | --------- | -------------------------- |
157
- | required | Ensures value is not empty |
158
- | match | Validates value via regex |
159
-
160
- ---
161
-
162
- ## 8. Functional Pipeline Execution
163
-
164
- Each variable pipeline is executed using reduce semantics:
165
-
166
- ```js
167
- pipeline.reduce((acc, step) => {
168
- return transforms[step.fn](acc, ...step.args);
169
- }, baseValue);
170
- ```
171
-
172
- All transforms must be registered in a dictionary:
173
-
174
- ```js
175
- const transforms = {
176
- slugify,
177
- lowercase,
178
- uppercase,
179
- max,
180
- trim,
181
- };
182
- ```
183
-
184
- ---
185
-
186
- ## 9. Git Ref Sanitization
187
-
188
- After full pattern rendering, a final sanitization step must run:
189
-
190
- - Remove invalid Git characters
191
- - Prevent:
192
- - trailing slash
193
- - double dots
194
- - leading dash
195
- - spaces
196
- - Ensure valid Git ref format
197
-
198
- Final step example:
199
-
200
- ```js
201
- branchName = sanitizeGitRef(branchName);
202
- ```
203
-
204
- ---
205
-
206
- ## 10. Branch Type Standardization
207
-
208
- Supported types:
209
-
210
- - feat
211
- - fix
212
- - chore
213
- - docs
214
- - refactor
215
- - test
216
- - perf
217
- - build
218
- - ci
219
-
220
- Optional alias mapping:
221
-
222
- - feature → feat
223
- - bugfix → fix
224
-
225
- ---
226
-
227
- ## 11. Interactive Mode Behavior
228
-
229
- If required values are missing:
230
-
231
- Prompt user for: - type - id - title
232
-
233
- Validation must occur immediately after input.
234
-
235
- ---
236
-
237
- ## 12. Optional Flags
238
-
239
- | Flag | Description |
240
- | ---------- | ------------------------------------------ |
241
- | `--create` | Creates branch using `git switch -c` |
242
- | `--print` | Prints branch name only (default behavior) |
243
-
244
- ---
245
-
246
- ## 13. Example Outputs
247
-
248
- Input:
249
-
250
- type = feat
251
- title = My very interesting task
252
- id = STK-123
253
-
254
- Pattern:
255
-
256
- {type}/{title:slugify;max:25}-{id}
257
-
258
- Output:
259
-
260
- feat/my-very-interesting-task-STK-123
1
+ # new-branch
261
2
 
262
- ---
3
+ <p align="center">
4
+ <img src="./logo.svg" width="180" alt="new-branch logo" />
5
+ </p>
263
6
 
264
- ## 14. Future Extensions
7
+ A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
265
8
 
266
- - Custom transform plugins
267
- - Jira integration (auto-fetch title from ID)
268
- - Branch existence check
269
- - Automatic incremental suffixing
270
- - Conventional commits integration
9
+ ![demo](./demo.gif)
271
10
 
272
11
  ---
273
12
 
274
- ## 15. Summary
275
-
276
- `new-branch` is a composable, functional, extensible CLI tool for
277
- standardized Git branch naming.
278
-
279
- Core principles:
280
-
281
- - Functional pipeline transforms
282
- - Deterministic output
283
- - Git-safe sanitization
284
- - Clear precedence rules
285
- - Interactive fallback
286
-
287
- # new-branch
13
+ ## Why
288
14
 
289
- A small CLI tool to generate and optionally create standardized Git branch names
290
- based on a composable pattern language.
15
+ Keep branch names consistent across your team using a declarative pattern language.
291
16
 
292
17
  ---
293
18
 
294
- ## Installation
19
+ ## Install
295
20
 
296
- Using npx:
21
+ Run without installing:
297
22
 
298
23
  ```bash
299
24
  npx new-branch
@@ -309,78 +34,32 @@ npm install -g new-branch
309
34
 
310
35
  ## Usage
311
36
 
312
- ### Generate a branch name
37
+ Generate a branch name:
313
38
 
314
39
  ```bash
315
40
  new-branch \
316
- --pattern "{type}/{title:slugify}-{id}" \
41
+ --pattern "{type}/{title:slugify;max:25}-{id}" \
317
42
  --type feat \
318
43
  --title "My task" \
319
44
  --id STK-123
320
45
  ```
321
46
 
322
- Output:
323
-
324
- ```
325
- feat/minha-tarefa-STK-123
326
- ```
327
-
328
- ---
329
-
330
- ### Create the branch automatically
47
+ Create the branch automatically:
331
48
 
332
49
  ```bash
333
50
  new-branch \
334
- --pattern "{type:upper}/{title:slugify}-{id}" \
51
+ --pattern "{type}/{title:slugify}-{id}" \
335
52
  --type feat \
336
53
  --title "My task" \
337
54
  --id STK-123 \
338
55
  --create
339
56
  ```
340
57
 
341
- This runs:
342
-
343
- ```
344
- git switch -c <generated-branch>
345
- ```
346
-
347
- ---
348
-
349
- ### Interactive mode
350
-
351
- If required variables in the pattern are missing, the CLI will prompt for them.
352
-
353
- Example:
354
-
355
- ```bash
356
- new-branch \
357
- --pattern "{type:upper}/{title:slugify}-{id}" \
358
- --title "My task" \
359
- --id STK-123 \
360
- --create
361
- ```
362
-
363
- You will be prompted for `type`.
364
-
365
- ---
366
-
367
- ## CLI Options
368
-
369
- | Option | Description |
370
- | ------------------------- | -------------------------------------------- |
371
- | `-p, --pattern <pattern>` | Branch name pattern |
372
- | `--id <id>` | Task ID |
373
- | `--title <title>` | Task title |
374
- | `--type <type>` | Branch type |
375
- | `--create` | Create the branch using `git switch -c` |
376
- | `--no-prompt` | Fail instead of prompting for missing values |
377
- | `--quiet` | Do not print any output |
378
-
379
58
  ---
380
59
 
381
60
  ## Pattern Language
382
61
 
383
- Branch names are generated from a pattern string.
62
+ Patterns are composed of variables and ordered transforms.
384
63
 
385
64
  Example:
386
65
 
@@ -395,15 +74,14 @@ Example:
395
74
  ```
396
75
 
397
76
  - Variables are wrapped in `{}`
398
- - Transforms are separated by `;`
399
- - Transform arguments are separated by `:`
77
+ - Transforms run left-to-right
78
+ - Multiple transforms are separated by `;`
79
+ - Transform arguments use `:`
400
80
 
401
81
  ---
402
82
 
403
83
  ## Built-in Variables
404
84
 
405
- These variables are currently supported:
406
-
407
85
  - `type`
408
86
  - `title`
409
87
  - `id`
@@ -412,73 +90,86 @@ These variables are currently supported:
412
90
 
413
91
  ## Built-in Transforms
414
92
 
415
- All transforms are pure functions and executed left-to-right.
93
+ | Transform | Description |
94
+ | --------- | -------------------------- |
95
+ | `slugify` | Convert to URL-safe slug |
96
+ | `lower` | Convert to lowercase |
97
+ | `upper` | Convert to uppercase |
98
+ | `camel` | Convert to camelCase |
99
+ | `kebab` | Convert to kebab-case |
100
+ | `snake` | Convert to snake_case |
101
+ | `title` | Convert to Title Case |
102
+ | `words:n` | Keep at most `n` words |
103
+ | `max:n` | Truncate to `n` characters |
416
104
 
417
- ### String transforms
105
+ All transforms are pure functions and composable.
418
106
 
419
- | Transform | Description |
420
- | --------- | --------------------------- |
421
- | `lower` | Lowercases the value |
422
- | `upper` | Uppercases the value |
423
- | `slugify` | Converts to a git-safe slug |
107
+ ---
424
108
 
425
- ### Argument-based transforms
109
+ ## Interactive Mode
426
110
 
427
- | Transform | Description | Example |
428
- | --------- | ------------------------------ | -------- |
429
- | `max` | Truncates string to max length | `max:25` |
111
+ If variables referenced by the pattern are missing, the CLI prompts for them by default.
430
112
 
431
- Example:
113
+ Disable prompts with:
432
114
 
433
- ```
434
- {title:slugify;max:25}
115
+ ```bash
116
+ --no-prompt
435
117
  ```
436
118
 
437
119
  ---
438
120
 
439
- ## Git Ref Validation
440
-
441
- After rendering, branch names are:
442
-
443
- 1. Lightly sanitized
444
- 2. Validated using `git check-ref-format --branch`
121
+ ## CLI Options
445
122
 
446
- If invalid, the command fails.
123
+ | Option | Description |
124
+ | ------------------------- | ----------------------------------- |
125
+ | `-p, --pattern <pattern>` | Branch pattern |
126
+ | `--type <type>` | Branch type |
127
+ | `--title <title>` | Task title |
128
+ | `--id <id>` | Task identifier |
129
+ | `--create` | Create branch using `git switch -c` |
130
+ | `--no-prompt` | Fail instead of prompting |
131
+ | `--quiet` | Suppress output |
447
132
 
448
133
  ---
449
134
 
450
- ## Example
135
+ ## Project Configuration
451
136
 
452
- Input:
137
+ You can define a default pattern in `package.json`:
453
138
 
454
- ```
455
- type = feat
456
- title = My task
457
- id = STK-123
139
+ ```json
140
+ {
141
+ "new-branch": {
142
+ "pattern": "{type}/{title:slugify}-{id}"
143
+ }
144
+ }
458
145
  ```
459
146
 
460
- Pattern:
147
+ Resolution order:
461
148
 
462
- ```
463
- {type}/{title:slugify;max:25}-{id}
464
- ```
149
+ 1. CLI flags
150
+ 2. `package.json` configuration
151
+ 3. Interactive prompt (if enabled)
465
152
 
466
- Output:
153
+ ---
467
154
 
468
- ```
469
- feat/my-task-STK-123
470
- ```
155
+ ## Git Safety
471
156
 
472
- ---
157
+ After rendering, branch names are:
473
158
 
474
- ## Roadmap
159
+ 1. Lightly sanitized
160
+ 2. Validated via `git check-ref-format --branch`
161
+
162
+ Invalid names cause the command to fail.
163
+
164
+ ---
475
165
 
476
- Planned future features:
166
+ ## Development
477
167
 
478
- - Config file support
479
- - Custom transform plugins
480
- - Additional variable providers (e.g. git username, date)
481
- - Branch existence checks
168
+ ```bash
169
+ pnpm install
170
+ pnpm test:run
171
+ pnpm build
172
+ ```
482
173
 
483
174
  ---
484
175
 
package/dist/cli.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "new-branch",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Generate and create standardized git branch names from a pattern.",
5
5
  "keywords": [
6
6
  "git",
@@ -1,34 +0,0 @@
1
- import { vi, describe, it, expect, beforeEach } from "vitest";
2
- vi.mock("node:fs/promises", () => ({
3
- readFile: vi.fn(),
4
- }));
5
- import { readFile } from "node:fs/promises";
6
- describe("loadProjectConfig", () => {
7
- beforeEach(() => {
8
- vi.resetAllMocks();
9
- });
10
- it("returns pattern when package.json contains new-branch.pattern as string", async () => {
11
- readFile.mockResolvedValueOnce(JSON.stringify({ "new-branch": { pattern: "{type}/{title}-{id}" } }));
12
- const { loadProjectConfig } = await import("./loadProjectConfig.js");
13
- const cfg = await loadProjectConfig();
14
- expect(cfg).toEqual({ pattern: "{type}/{title}-{id}" });
15
- });
16
- it("returns empty object when package.json has no new-branch key", async () => {
17
- readFile.mockResolvedValueOnce(JSON.stringify({ name: "pkg" }));
18
- const { loadProjectConfig } = await import("./loadProjectConfig.js");
19
- const cfg = await loadProjectConfig();
20
- expect(cfg).toEqual({});
21
- });
22
- it("ignores non-string pattern values", async () => {
23
- readFile.mockResolvedValueOnce(JSON.stringify({ "new-branch": { pattern: 123 } }));
24
- const { loadProjectConfig } = await import("./loadProjectConfig.js");
25
- const cfg = await loadProjectConfig();
26
- expect(cfg).toEqual({});
27
- });
28
- it("returns empty object if reading package.json throws", async () => {
29
- readFile.mockRejectedValueOnce(new Error("enoent"));
30
- const { loadProjectConfig } = await import("./loadProjectConfig.js");
31
- const cfg = await loadProjectConfig();
32
- expect(cfg).toEqual({});
33
- });
34
- });
@@ -1,30 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { sanitizeGitRef } from "./sanitizeGitRef.js";
3
- describe("sanitizeGitRef", () => {
4
- it("trims input and replaces whitespace with dashes", () => {
5
- expect(sanitizeGitRef(" feature new ")).toBe("feature-new");
6
- });
7
- it("removes forbidden characters and sequences", () => {
8
- const input = "~^:??*[\\]name@{weird}";
9
- // forbidden chars removed and @{ removed
10
- expect(sanitizeGitRef(input)).toBe("nameweird}");
11
- });
12
- it("collapses multiple slashes and removes leading/trailing slashes", () => {
13
- expect(sanitizeGitRef("///a//b/c///")).toBe("a/b/c");
14
- });
15
- it("collapses repeated dots and trims trailing dots", () => {
16
- expect(sanitizeGitRef("v1..0...")).toBe("v1.0");
17
- });
18
- it("prevents leading dash or slash and trailing dot or slash", () => {
19
- expect(sanitizeGitRef("-/--abc/.")).toBe("abc");
20
- });
21
- it("removes trailing .lock suffix", () => {
22
- expect(sanitizeGitRef("release.lock")).toBe("release");
23
- // multiple .lock occurrences only remove final suffix
24
- expect(sanitizeGitRef("a.lock.lock")).toBe("a.lock");
25
- });
26
- it("works with an empty-ish result (keeps dot/punctuations according to sanitizer)", () => {
27
- // Implementation collapses dots and slashes but does not remove the leading dot
28
- expect(sanitizeGitRef(" ....///--- ")).toBe("./---");
29
- });
30
- });
@@ -1,50 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseArgs } from "./parseArgs.js";
3
- describe("parseArgs", () => {
4
- it("parses long options", () => {
5
- const argv = [
6
- "node",
7
- "cli",
8
- "--pattern",
9
- "{type}/{title}-{id}",
10
- "--id",
11
- "STK-123",
12
- "--title",
13
- "Minha tarefa",
14
- "--type",
15
- "feat",
16
- "--create",
17
- ];
18
- const res = parseArgs(argv);
19
- expect(res.options.pattern).toBe("{type}/{title}-{id}");
20
- expect(res.options.id).toBe("STK-123");
21
- expect(res.options.title).toBe("Minha tarefa");
22
- expect(res.options.type).toBe("feat");
23
- expect(res.options.create).toBe(true);
24
- expect(res.args).toEqual([]);
25
- });
26
- it("parses short -p as pattern", () => {
27
- const argv = ["node", "cli", "-p", "{type}/{title}-{id}"];
28
- const res = parseArgs(argv);
29
- expect(res.options.pattern).toBe("{type}/{title}-{id}");
30
- });
31
- it("strips tsx double-dash separator", () => {
32
- const argv = [
33
- "node",
34
- "cli",
35
- "--",
36
- "--pattern",
37
- "{type}/{title}-{id}",
38
- "--id",
39
- "STK-123",
40
- ];
41
- const res = parseArgs(argv);
42
- expect(res.options.pattern).toBe("{type}/{title}-{id}");
43
- expect(res.options.id).toBe("STK-123");
44
- });
45
- it("supports --no-prompt", () => {
46
- const argv = ["node", "cli", "--no-prompt"];
47
- const res = parseArgs(argv);
48
- expect(res.options.prompt).toBe(false);
49
- });
50
- });
@@ -1,25 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parsePattern } from "./parsePattern.js";
3
- describe("parsePattern", () => {
4
- it("parses literals + variables + transforms", () => {
5
- const res = parsePattern("{type}/{title:slugify;max:25}-{id}");
6
- expect(res.variablesUsed).toEqual(["type", "title", "id"]);
7
- expect(res.nodes).toEqual([
8
- { kind: "variable", name: "type", transforms: [] },
9
- { kind: "literal", value: "/" },
10
- {
11
- kind: "variable",
12
- name: "title",
13
- transforms: [
14
- { name: "slugify", args: [] },
15
- { name: "max", args: ["25"] },
16
- ],
17
- },
18
- { kind: "literal", value: "-" },
19
- { kind: "variable", name: "id", transforms: [] },
20
- ]);
21
- });
22
- it("throws on missing closing brace", () => {
23
- expect(() => parsePattern("{type/{id}")).toThrow(/missing "}"|Invalid pattern/);
24
- });
25
- });
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { camel } from "./camel.js";
3
- describe("camel transform", () => {
4
- it("converts spaced text to camelCase", () => {
5
- expect(camel.fn("My Task", [])).toBe("myTask");
6
- });
7
- it("handles punctuation and multiple separators", () => {
8
- expect(camel.fn("hello-world_test", [])).toBe("helloWorldTest");
9
- });
10
- it("returns empty string for empty input", () => {
11
- expect(camel.fn("", [])).toBe("");
12
- });
13
- });
@@ -1,35 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { splitWords, upperFirst, lowerFirst } from "./words.js";
3
- describe("splitWords", () => {
4
- it("returns empty array for empty or whitespace-only input", () => {
5
- expect(splitWords("")).toEqual([]);
6
- expect(splitWords(" ")).toEqual([]);
7
- });
8
- it("splits on separators and trims words", () => {
9
- expect(splitWords("hello world-test")).toEqual(["hello", "world", "test"]);
10
- expect(splitWords(" leading and trailing ")).toEqual(["leading", "and", "trailing"]);
11
- });
12
- it("handles camelCase boundaries", () => {
13
- expect(splitWords("myTask")).toEqual(["my", "Task"]);
14
- expect(splitWords("version2Beta")).toEqual(["version2", "Beta"]);
15
- });
16
- it("handles ALLCAPS followed by capitalized word", () => {
17
- expect(splitWords("HTTPServer")).toEqual(["HTTP", "Server"]);
18
- expect(splitWords("XMLHttpRequest")).toEqual(["XML", "Http", "Request"]);
19
- });
20
- it("keeps Unicode letters and accents", () => {
21
- expect(splitWords("Título grande")).toEqual(["Título", "grande"]);
22
- });
23
- });
24
- describe("upperFirst / lowerFirst", () => {
25
- it("upperFirst capitalizes only the first character", () => {
26
- expect(upperFirst("hello")).toBe("Hello");
27
- expect(upperFirst("")).toBe("");
28
- expect(upperFirst("éclair")).toBe("Éclair");
29
- });
30
- it("lowerFirst lowercases only the first character", () => {
31
- expect(lowerFirst("Hello")).toBe("hello");
32
- expect(lowerFirst("")).toBe("");
33
- expect(lowerFirst("Éclair")).toBe("éclair");
34
- });
35
- });
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { kebab } from "./kebab.js";
3
- describe("kebab transform", () => {
4
- it("converts spaced text to kebab-case", () => {
5
- expect(kebab.fn("My Task", [])).toBe("my-task");
6
- });
7
- it("handles camelCase and punctuation", () => {
8
- expect(kebab.fn("myTaskHTTP Server", [])).toBe("my-task-http-server");
9
- });
10
- it("returns empty string for empty input", () => {
11
- expect(kebab.fn("", [])).toBe("");
12
- });
13
- });
@@ -1,10 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { lower } from "./lower.js";
3
- describe("lower transform", () => {
4
- it("converts to lowercase", () => {
5
- expect(lower.fn("Hello WORLD", [])).toBe("hello world");
6
- });
7
- it("keeps non-letter characters", () => {
8
- expect(lower.fn("123-ÁÉ!", [])).toBe("123-áé!");
9
- });
10
- });
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { max } from "./max.js";
3
- describe("max transform", () => {
4
- it("truncates the value to the given length", () => {
5
- expect(max.fn("abcdef", ["3"])).toBe("abc");
6
- expect(max.fn("short", ["10"])).toBe("short");
7
- });
8
- it("throws on negative or non-finite sizes", () => {
9
- expect(() => max.fn("abc", ["-1"])).toThrow();
10
- expect(() => max.fn("abc", ["not-a-number"])).toThrow();
11
- expect(() => max.fn("abc", [])).toThrow();
12
- });
13
- });
@@ -1,46 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { renderPattern } from "./renderPattern.js";
3
- import { parsePattern } from "../parsePattern.js";
4
- import { lower } from "./lower.js";
5
- import { upper } from "./upper.js";
6
- import { max } from "./max.js";
7
- import { slugify } from "./slugify.js";
8
- const defaultTransforms = {
9
- [lower.name]: lower.fn,
10
- [upper.name]: upper.fn,
11
- [max.name]: max.fn,
12
- [slugify.name]: slugify.fn,
13
- };
14
- describe("renderPattern", () => {
15
- it("renders literals and variables without transforms", () => {
16
- const parsed = parsePattern("{type}/{title}-{id}");
17
- const out = renderPattern(parsed, { type: "feat", title: "My Task", id: "123" }, { transforms: defaultTransforms });
18
- expect(out).toBe("feat/My Task-123");
19
- });
20
- it("applies transforms in order (slugify then max)", () => {
21
- const parsed = parsePattern("{title:slugify;max:5}");
22
- const out = renderPattern(parsed, { title: "Título grande" }, { transforms: defaultTransforms });
23
- // slugify -> "titulo-grande" then max:5 -> "titul"
24
- expect(out).toBe("titul");
25
- });
26
- it("throws when an unknown transform is used", () => {
27
- const parsed = {
28
- nodes: [{ kind: "variable", name: "foo", transforms: [{ name: "nope", args: [] }] }],
29
- variablesUsed: ["foo"],
30
- };
31
- expect(() => renderPattern(parsed, { foo: "bar" }, { transforms: defaultTransforms })).toThrow(/Unknown transform/i);
32
- });
33
- it("throws on missing variable in strict mode (default)", () => {
34
- const parsed = parsePattern("{id}");
35
- expect(() => renderPattern(parsed, {}, { transforms: defaultTransforms })).toThrow(/Missing value/);
36
- });
37
- it("returns empty for missing variable when strict is false", () => {
38
- const parsed = parsePattern("pre-{id}-post");
39
- const out = renderPattern(parsed, {}, { transforms: defaultTransforms, strict: false });
40
- expect(out).toBe("pre--post");
41
- });
42
- it("propagates transform errors (invalid max arg)", () => {
43
- const parsed = parsePattern("{title:max:-1}");
44
- expect(() => renderPattern(parsed, { title: "abc" }, { transforms: defaultTransforms })).toThrow();
45
- });
46
- });
@@ -1,14 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { slugify } from "./slugify.js";
3
- describe("slugify transform", () => {
4
- it("removes accents, lowercases and replaces non-alphanum with dashes", () => {
5
- const input = "Título com Acentos & símbolos!";
6
- const out = slugify.fn(input, []);
7
- expect(out).toBe("titulo-com-acentos-simbolos");
8
- });
9
- it("collapses multiple separators into one and trims dashes", () => {
10
- const input = " --Hello___World-- ";
11
- const out = slugify.fn(input, []);
12
- expect(out).toBe("hello-world");
13
- });
14
- });
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { snake } from "./snake.js";
3
- describe("snake transform", () => {
4
- it("converts spaced text to snake_case", () => {
5
- expect(snake.fn("My Task", [])).toBe("my_task");
6
- });
7
- it("handles camelCase and punctuation", () => {
8
- expect(snake.fn("myTaskHTTP Server", [])).toBe("my_task_http_server");
9
- });
10
- it("returns empty string for empty input", () => {
11
- expect(snake.fn("", [])).toBe("");
12
- });
13
- });
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { title } from "./title.js";
3
- describe("title transform", () => {
4
- it("converts text to Title Case", () => {
5
- expect(title.fn("hello WORLD", [])).toBe("Hello World");
6
- });
7
- it("handles punctuation and multiple separators", () => {
8
- expect(title.fn("my-task_HTTP server", [])).toBe("My Task Http Server");
9
- });
10
- it("returns empty string for empty input", () => {
11
- expect(title.fn("", [])).toBe("");
12
- });
13
- });
@@ -1,10 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { upper } from "./upper.js";
3
- describe("upper transform", () => {
4
- it("converts to uppercase", () => {
5
- expect(upper.fn("Hello world", [])).toBe("HELLO WORLD");
6
- });
7
- it("keeps non-letter characters", () => {
8
- expect(upper.fn("123-áé!", [])).toBe("123-ÁÉ!");
9
- });
10
- });
@@ -1,16 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { words } from "./words.js";
3
- describe("words transform", () => {
4
- it("limits the number of words when a positive number is provided", () => {
5
- expect(words.fn("one two three four", ["2"]).trim()).toBe("one two");
6
- expect(words.fn("hello world", ["5"]).trim()).toBe("hello world");
7
- });
8
- it("returns empty string when 0 is provided", () => {
9
- expect(words.fn("some text here", ["0"]).trim()).toBe("");
10
- });
11
- it("throws when argument is missing or invalid", () => {
12
- expect(() => words.fn("a b c", [])).toThrow();
13
- expect(() => words.fn("a b c", ["-1"])).toThrow();
14
- expect(() => words.fn("a b c", ["not-a-number"])).toThrow();
15
- });
16
- });
@@ -1,64 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { resolveMissingValues } from "./resolveMissingValues.js";
3
- import { parsePattern } from "../pattern/parsePattern.js";
4
- // Mock prompts module so tests can control interactive behavior
5
- vi.mock("@inquirer/prompts", () => {
6
- return {
7
- input: vi.fn(),
8
- select: vi.fn(),
9
- };
10
- });
11
- // Provide a lightweight mock for the runtime enums so module resolution
12
- // does not fail in the test environment (we only need the shape used
13
- // by the code under test).
14
- vi.mock("@/runtime/enums.js", () => ({
15
- TYPE_CHOICES: [
16
- { name: "Feature", value: "feat" },
17
- { name: "Fix", value: "fix" },
18
- ],
19
- }));
20
- import { input, select } from "@inquirer/prompts";
21
- beforeEach(() => {
22
- // resetAllMocks clears implementations and mock queues (e.g. mockResolvedValueOnce)
23
- // which prevents cross-test leakage of one-off mock return values.
24
- vi.resetAllMocks();
25
- });
26
- describe("resolveMissingValues", () => {
27
- it("returns values unchanged when all required vars are present", async () => {
28
- const parsed = parsePattern("{type}/{title}-{id}");
29
- const initial = { type: "feat", title: "My Task", id: "123" };
30
- const out = await resolveMissingValues(parsed, initial, { prompt: true });
31
- expect(out).toEqual(initial);
32
- expect(input).not.toHaveBeenCalled();
33
- expect(select).not.toHaveBeenCalled();
34
- });
35
- it("prompts for a missing non-type variable using input", async () => {
36
- const parsed = parsePattern("{type}/{title}");
37
- // input should be used for `title`
38
- input.mockResolvedValueOnce("Provided Title");
39
- // Type is present so select should not be called
40
- const out = await resolveMissingValues(parsed, { type: "fix" }, { prompt: true });
41
- expect(input).toHaveBeenCalled();
42
- expect(select).not.toHaveBeenCalled();
43
- expect(out.title).toBe("Provided Title");
44
- });
45
- it("uses select for the `type` variable", async () => {
46
- const parsed = parsePattern("{type}/{title}");
47
- select.mockResolvedValueOnce("feat");
48
- input.mockResolvedValueOnce("Some title");
49
- const out = await resolveMissingValues(parsed, { title: "Some title" }, { prompt: true });
50
- expect(select).toHaveBeenCalled();
51
- expect(out.type).toBe("feat");
52
- });
53
- it("throws when prompt is false and a required variable is missing", async () => {
54
- const parsed = parsePattern("{id}");
55
- await expect(resolveMissingValues(parsed, {}, { prompt: false })).rejects.toThrow(/Missing required value/i);
56
- });
57
- it("treats whitespace-only values as missing and prompts", async () => {
58
- const parsed = parsePattern("{title}");
59
- input.mockResolvedValueOnce("Trimmed");
60
- const out = await resolveMissingValues(parsed, { title: " " }, { prompt: true });
61
- expect(input).toHaveBeenCalled();
62
- expect(out.title).toBe("Trimmed");
63
- });
64
- });