@pc360/chlog 0.1.11 → 0.2.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/CHANGELOG.md +12 -0
- package/README.md +16 -61
- package/bin/chlog.js +29 -7
- package/lib/chlog-lib.js +11 -22
- package/lib/chlog-prompts.js +200 -0
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
|
|
|
10
10
|
|
|
11
11
|
- No changes yet.
|
|
12
12
|
|
|
13
|
+
## [0.2.0] - 2026-02-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added graceful interactive cancel handling: pressing `Ctrl + C` now exits cleanly with `Operation cancelled by user.`
|
|
18
|
+
- Added unit tests for prompt cancellation detection.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Updated README CLI rules to document interactive cancellation behavior and the exact cancellation message.
|
|
23
|
+
- Simplified unit tests by removing redundant parser test cases already covered by CLI integration tests.
|
|
24
|
+
|
|
13
25
|
## [0.1.11] - 2026-02-15
|
|
14
26
|
|
|
15
27
|
### Added
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npm i -g @pc360/chlog
|
|
|
36
36
|
Alternative (without global install):
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
npx @pc360/chlog
|
|
39
|
+
npx @pc360/chlog
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
---
|
|
@@ -46,19 +46,13 @@ npx @pc360/chlog --type <added|changed|fixed|removed> --scope <scope> --message
|
|
|
46
46
|
For first use, always run a dry run to preview output without creating a file:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
chlog --
|
|
49
|
+
chlog --dry-run
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Then run interactively:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
chlog
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
If the preview is correct, run the actual command:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
chlog --type added --scope JES-99 --message "Test entry"
|
|
55
|
+
chlog
|
|
62
56
|
```
|
|
63
57
|
|
|
64
58
|
---
|
|
@@ -194,56 +188,14 @@ chlog --version
|
|
|
194
188
|
|
|
195
189
|
---
|
|
196
190
|
|
|
197
|
-
# Usage
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
chlog --type <added|changed|fixed|removed> \
|
|
201
|
-
--scope <scope> \
|
|
202
|
-
--message "<description>" \
|
|
203
|
-
[--frontend]
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Short form:
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
chlog -t <added|changed|fixed|removed> -s <scope> -m "<description>" [-f]
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
191
|
# Parameters
|
|
215
192
|
|
|
216
|
-
| Flag
|
|
217
|
-
|
|
|
218
|
-
| `-
|
|
219
|
-
|
|
|
220
|
-
| `-
|
|
221
|
-
| `-
|
|
222
|
-
| `-d, --dry-run` | No | Preview file creation without writing |
|
|
223
|
-
| `-h, --help` | No | Show help |
|
|
224
|
-
| `-v, --version` | No | Show CLI version |
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
|
-
# Examples
|
|
229
|
-
|
|
230
|
-
### Back-End (default)
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
chlog --type added --scope JES-33 --message "Add daily consolidated JE job"
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### Front-End
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
chlog --type fixed --scope JES-45 --frontend --message "Fix table pagination issue"
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Module Scope
|
|
243
|
-
|
|
244
|
-
```bash
|
|
245
|
-
chlog --type changed --scope "Credit Validation" --message "Improve credit limit computation"
|
|
246
|
-
```
|
|
193
|
+
| Flag | Required | Description |
|
|
194
|
+
| --------------- | -------- | -------------------------------------------------------- |
|
|
195
|
+
| `-d, --dry-run` | No | Preview file creation without writing |
|
|
196
|
+
| `--tz` | No | Timezone for filename timestamp (default: `Asia/Manila`) |
|
|
197
|
+
| `-h, --help` | No | Show help |
|
|
198
|
+
| `-v, --version` | No | Show CLI version |
|
|
247
199
|
|
|
248
200
|
---
|
|
249
201
|
|
|
@@ -270,9 +222,12 @@ Timezone used: `Asia/Manila`
|
|
|
270
222
|
|
|
271
223
|
# CLI Rules
|
|
272
224
|
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
225
|
+
- Type, scope, message, and layer are collected interactively
|
|
226
|
+
- Type is selected inline/horizontally (`added / changed / fixed / removed`)
|
|
227
|
+
- Layer uses a `Is frontend?` toggle (`No` by default = `Back-End`)
|
|
228
|
+
- After message input in interactive mode, a toggle asks `Display preview?` (`Yes/No`)
|
|
229
|
+
- If preview is shown, a second toggle asks `Do you want to create changelog?` (`Yes/No`)
|
|
230
|
+
- Pressing `Ctrl + C` during interactive prompts cancels the operation and prints `Operation cancelled by user.`
|
|
276
231
|
- Existing files are not overwritten
|
|
277
232
|
- Layer tag is auto-added (`[Back-End]` or `[Front-End]`)
|
|
278
233
|
|
package/bin/chlog.js
CHANGED
|
@@ -17,6 +17,12 @@ const {
|
|
|
17
17
|
renderSuccess,
|
|
18
18
|
renderError,
|
|
19
19
|
} = require("../lib/chlog-ui");
|
|
20
|
+
const {
|
|
21
|
+
promptForMissingArgs,
|
|
22
|
+
promptYesNo,
|
|
23
|
+
isInteractiveTerminal,
|
|
24
|
+
isPromptCancelled,
|
|
25
|
+
} = require("../lib/chlog-prompts");
|
|
20
26
|
|
|
21
27
|
function getHelpText() {
|
|
22
28
|
return `
|
|
@@ -29,14 +35,10 @@ Each entry is formatted as:
|
|
|
29
35
|
[<scope>] [Back-End|Front-End] <message>
|
|
30
36
|
|
|
31
37
|
Usage:
|
|
32
|
-
chlog
|
|
33
|
-
chlog -t <added|changed|fixed|removed> -s <ticket> -m "<text>" [-f]
|
|
38
|
+
chlog
|
|
34
39
|
|
|
35
40
|
Options:
|
|
36
|
-
|
|
37
|
-
-s, --scope <ticket> Jira ticket or scope (required)
|
|
38
|
-
-m, --message "<msg>" Entry message (required)
|
|
39
|
-
-f, --frontend Use [Front-End] instead of [Back-End]
|
|
41
|
+
(type, scope, message, and layer are collected interactively)
|
|
40
42
|
--tz "<iana>" Timezone (default: Asia/Manila)
|
|
41
43
|
-d, --dry-run Preview only
|
|
42
44
|
-h, --help Show help
|
|
@@ -46,7 +48,7 @@ Options:
|
|
|
46
48
|
|
|
47
49
|
async function main() {
|
|
48
50
|
try {
|
|
49
|
-
|
|
51
|
+
let args = parseArgs(process.argv);
|
|
50
52
|
|
|
51
53
|
if (args.help) {
|
|
52
54
|
await renderHelp(getHelpText());
|
|
@@ -58,6 +60,10 @@ async function main() {
|
|
|
58
60
|
process.exit(0);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
if (isInteractiveTerminal()) {
|
|
64
|
+
args = await promptForMissingArgs(args);
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
validateArgs(args);
|
|
62
68
|
|
|
63
69
|
const timestamp = generateTimestamp(new Date(), args.tz);
|
|
@@ -82,6 +88,17 @@ async function main() {
|
|
|
82
88
|
process.exit(0);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
if (isInteractiveTerminal() && !args.dryRunSet) {
|
|
92
|
+
const shouldPreview = await promptYesNo("Display preview?", false);
|
|
93
|
+
if (shouldPreview) {
|
|
94
|
+
await renderDryRun({ entryPath, content });
|
|
95
|
+
const shouldExecute = await promptYesNo("Do you want to create changelog?", true);
|
|
96
|
+
if (!shouldExecute) {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
await fs.mkdir(entryDir, { recursive: true });
|
|
86
103
|
|
|
87
104
|
await fs.writeFile(entryPath, content, {
|
|
@@ -91,6 +108,11 @@ async function main() {
|
|
|
91
108
|
|
|
92
109
|
await renderSuccess({ entryPath });
|
|
93
110
|
} catch (err) {
|
|
111
|
+
if (isPromptCancelled(err)) {
|
|
112
|
+
process.stdout.write("Operation cancelled by user.\n");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
await renderError(err);
|
|
95
117
|
process.exit(1);
|
|
96
118
|
}
|
package/lib/chlog-lib.js
CHANGED
|
@@ -9,7 +9,9 @@ function parseArgs(argv) {
|
|
|
9
9
|
message: null,
|
|
10
10
|
tz: "Asia/Manila",
|
|
11
11
|
frontend: false,
|
|
12
|
+
layerSet: false,
|
|
12
13
|
dryRun: false,
|
|
14
|
+
dryRunSet: false,
|
|
13
15
|
help: false,
|
|
14
16
|
version: false,
|
|
15
17
|
};
|
|
@@ -29,26 +31,7 @@ function parseArgs(argv) {
|
|
|
29
31
|
|
|
30
32
|
if (arg === "--dry-run" || arg === "-d") {
|
|
31
33
|
args.dryRun = true;
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (arg === "--frontend" || arg === "-f") {
|
|
36
|
-
args.frontend = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (arg === "--type" || arg === "-t") {
|
|
41
|
-
args.type = argv[++i];
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (arg === "--scope" || arg === "-s") {
|
|
46
|
-
args.scope = argv[++i];
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (arg === "--message" || arg === "-m") {
|
|
51
|
-
args.message = argv[++i];
|
|
34
|
+
args.dryRunSet = true;
|
|
52
35
|
continue;
|
|
53
36
|
}
|
|
54
37
|
|
|
@@ -67,13 +50,19 @@ function validateArgs(args) {
|
|
|
67
50
|
if (!args.type || !VALID_TYPES.has(args.type)) {
|
|
68
51
|
throw new Error(`--type must be one of: added, changed, fixed, removed`);
|
|
69
52
|
}
|
|
70
|
-
|
|
53
|
+
const normalizedScope = String(args.scope ?? "").trim();
|
|
54
|
+
const normalizedMessage = String(args.message ?? "").trim();
|
|
55
|
+
|
|
56
|
+
if (!normalizedScope) {
|
|
71
57
|
throw new Error(`--scope is required (e.g. JES-33)`);
|
|
72
58
|
}
|
|
73
|
-
if (!
|
|
59
|
+
if (!normalizedMessage) {
|
|
74
60
|
throw new Error(`--message is required`);
|
|
75
61
|
}
|
|
76
62
|
|
|
63
|
+
args.scope = normalizedScope;
|
|
64
|
+
args.message = normalizedMessage;
|
|
65
|
+
|
|
77
66
|
// Validate timezone (throws RangeError if invalid)
|
|
78
67
|
new Intl.DateTimeFormat("en-US", { timeZone: args.tz }).format(new Date());
|
|
79
68
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { VALID_TYPES } = require("./chlog-lib");
|
|
4
|
+
|
|
5
|
+
class InlineChoicePrompt {
|
|
6
|
+
constructor({ message, choices, initial = 0 }) {
|
|
7
|
+
const Prompt = require("enquirer/lib/prompt");
|
|
8
|
+
|
|
9
|
+
this.Prompt = class extends Prompt {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.cursorHide();
|
|
13
|
+
this.choices = options.choices.map((choice) =>
|
|
14
|
+
typeof choice === "string"
|
|
15
|
+
? { name: choice, message: choice }
|
|
16
|
+
: { name: choice.name, message: choice.message ?? choice.name },
|
|
17
|
+
);
|
|
18
|
+
this.index = Math.max(
|
|
19
|
+
0,
|
|
20
|
+
Math.min(options.initial ?? 0, this.choices.length - 1),
|
|
21
|
+
);
|
|
22
|
+
this.value = this.choices[this.index]?.name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
left() {
|
|
26
|
+
if (this.index <= 0) {
|
|
27
|
+
this.alert();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.index -= 1;
|
|
31
|
+
this.value = this.choices[this.index].name;
|
|
32
|
+
this.render();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
right() {
|
|
36
|
+
if (this.index >= this.choices.length - 1) {
|
|
37
|
+
this.alert();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.index += 1;
|
|
41
|
+
this.value = this.choices[this.index].name;
|
|
42
|
+
this.render();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
up() {
|
|
46
|
+
this.left();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
down() {
|
|
50
|
+
this.right();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
number(input) {
|
|
54
|
+
const nextIndex = Number(input) - 1;
|
|
55
|
+
if (
|
|
56
|
+
Number.isNaN(nextIndex) ||
|
|
57
|
+
nextIndex < 0 ||
|
|
58
|
+
nextIndex >= this.choices.length
|
|
59
|
+
) {
|
|
60
|
+
this.alert();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.index = nextIndex;
|
|
64
|
+
this.value = this.choices[this.index].name;
|
|
65
|
+
this.render();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
format() {
|
|
69
|
+
return this.choices
|
|
70
|
+
.map((choice, idx) => {
|
|
71
|
+
if (idx === this.index) {
|
|
72
|
+
return this.styles.primary.underline(choice.message);
|
|
73
|
+
}
|
|
74
|
+
return this.styles.muted(choice.message);
|
|
75
|
+
})
|
|
76
|
+
.join(this.styles.muted(" / "));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async render() {
|
|
80
|
+
const { size } = this.state;
|
|
81
|
+
|
|
82
|
+
const header = await this.header();
|
|
83
|
+
const prefix = await this.prefix();
|
|
84
|
+
const separator = await this.separator();
|
|
85
|
+
const message = await this.message();
|
|
86
|
+
const output = this.format();
|
|
87
|
+
const help =
|
|
88
|
+
(await this.error()) ||
|
|
89
|
+
this.styles.muted("(left/right or 1-4, enter)");
|
|
90
|
+
const footer = await this.footer();
|
|
91
|
+
|
|
92
|
+
let prompt = [prefix, message, separator, output].join(" ");
|
|
93
|
+
this.state.prompt = prompt;
|
|
94
|
+
if (help && !prompt.includes(help)) {
|
|
95
|
+
prompt += " " + help;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.clear(size);
|
|
99
|
+
this.write([header, prompt, footer].filter(Boolean).join("\n"));
|
|
100
|
+
this.write(this.margin[2]);
|
|
101
|
+
this.restore();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.options = { name: "inlineChoice", message, choices, initial };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async run() {
|
|
109
|
+
const prompt = new this.Prompt(this.options);
|
|
110
|
+
return prompt.run();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function promptForMissingArgs(args) {
|
|
115
|
+
const { Toggle, Input } = require("enquirer");
|
|
116
|
+
const nextArgs = { ...args };
|
|
117
|
+
|
|
118
|
+
if (!nextArgs.type) {
|
|
119
|
+
const typePrompt = new InlineChoicePrompt({
|
|
120
|
+
message: "Select changelog type:",
|
|
121
|
+
choices: Array.from(VALID_TYPES),
|
|
122
|
+
initial: 0,
|
|
123
|
+
});
|
|
124
|
+
nextArgs.type = await typePrompt.run();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!nextArgs.layerSet) {
|
|
128
|
+
const layerPrompt = new Toggle({
|
|
129
|
+
name: "frontend",
|
|
130
|
+
message: "Is frontend?",
|
|
131
|
+
enabled: "Yes",
|
|
132
|
+
disabled: "No",
|
|
133
|
+
initial: false,
|
|
134
|
+
});
|
|
135
|
+
nextArgs.frontend = Boolean(await layerPrompt.run());
|
|
136
|
+
nextArgs.layerSet = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!nextArgs.scope) {
|
|
140
|
+
const scopePrompt = new Input({
|
|
141
|
+
name: "scope",
|
|
142
|
+
message: "Enter scope/ticket (e.g. JES-33)",
|
|
143
|
+
validate: (value) =>
|
|
144
|
+
String(value ?? "").trim() ? true : "Scope is required",
|
|
145
|
+
});
|
|
146
|
+
nextArgs.scope = String(await scopePrompt.run()).trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!nextArgs.message) {
|
|
150
|
+
const messagePrompt = new Input({
|
|
151
|
+
name: "message",
|
|
152
|
+
message: "Enter changelog message",
|
|
153
|
+
validate: (value) =>
|
|
154
|
+
String(value ?? "").trim() ? true : "Message is required",
|
|
155
|
+
});
|
|
156
|
+
nextArgs.message = String(await messagePrompt.run()).trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return nextArgs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function promptYesNo(message, initial = false) {
|
|
163
|
+
const { Toggle } = require("enquirer");
|
|
164
|
+
const prompt = new Toggle({
|
|
165
|
+
name: "confirm",
|
|
166
|
+
message,
|
|
167
|
+
enabled: "Yes",
|
|
168
|
+
disabled: "No",
|
|
169
|
+
initial,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return Boolean(await prompt.run());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isInteractiveTerminal() {
|
|
176
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isPromptCancelled(error) {
|
|
180
|
+
if (error == null || error === "") {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (typeof error === "string") {
|
|
185
|
+
return /cancel(?:led|ed)?|abort(?:ed)?|interrupt(?:ed)?/i.test(error);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (error instanceof Error) {
|
|
189
|
+
return /cancel(?:led|ed)?|abort(?:ed)?|interrupt(?:ed)?/i.test(error.message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
promptForMissingArgs,
|
|
197
|
+
promptYesNo,
|
|
198
|
+
isInteractiveTerminal,
|
|
199
|
+
isPromptCancelled,
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pc360/chlog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "PC360 Changelog CLI Tool",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"changelog",
|
|
7
|
+
"cli"
|
|
8
|
+
],
|
|
9
|
+
"author": "almerleoalmazan@gmail.com",
|
|
6
10
|
"bin": {
|
|
7
11
|
"chlog": "bin/chlog.js"
|
|
8
12
|
},
|
|
@@ -21,6 +25,7 @@
|
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"@pc360/chlog": "^0.1.7",
|
|
28
|
+
"enquirer": "^2.4.1",
|
|
24
29
|
"ink": "^4.4.1",
|
|
25
30
|
"react": "^18.3.1"
|
|
26
31
|
},
|