@johnowennixon/diffdash 1.8.0 → 1.10.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 +8 -2
- package/dist/package.json +20 -18
- package/dist/src/lib_char_digit.js +1 -0
- package/dist/src/lib_cli.js +1 -57
- package/dist/src/lib_debug.js +1 -0
- package/dist/src/lib_diffdash_cli.js +3 -2
- package/dist/src/lib_diffdash_config.js +4 -3
- package/dist/src/lib_diffdash_llm.js +1 -0
- package/dist/src/lib_diffdash_sequence.js +47 -17
- package/dist/src/lib_git_message_generate.js +17 -15
- package/dist/src/lib_git_message_prompt.js +7 -3
- package/dist/src/lib_git_simple_staging.js +11 -6
- package/dist/src/lib_llm_chat.js +14 -9
- package/dist/src/lib_llm_config.js +8 -1
- package/dist/src/lib_llm_list.js +1 -1
- package/dist/src/lib_llm_model.js +196 -45
- package/dist/src/lib_llm_results.js +7 -4
- package/dist/src/lib_llm_tokens.js +6 -3
- package/dist/src/lib_parse_number.js +2 -2
- package/dist/src/lib_secret_check.js +109 -0
- package/dist/src/lib_text.js +39 -0
- package/dist/src/lib_tui_confirm.js +25 -0
- package/dist/src/lib_tui_none.js +4 -7
- package/dist/src/lib_tui_number.js +2 -2
- package/dist/src/lib_tui_table.js +2 -2
- package/package.json +20 -18
- package/dist/src/lib_tui_readline.js +0 -16
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { anyOf, createRegExp, digit, exactly, global, letter, oneOrMore, wordChar } from "magic-regexp";
|
|
2
|
+
import { ansi_yellow } from "./lib_ansi.js";
|
|
3
|
+
import { DASH } from "./lib_char_punctuation.js";
|
|
4
|
+
import { text_split_lines } from "./lib_text.js";
|
|
5
|
+
import { tui_confirm } from "./lib_tui_confirm.js";
|
|
6
|
+
import { tui_quote_smart_single } from "./lib_tui_quote.js";
|
|
7
|
+
const regexp_word_global = createRegExp(oneOrMore(anyOf(wordChar, exactly(DASH))), [global]);
|
|
8
|
+
const regexp_segment_global = createRegExp(oneOrMore(anyOf(letter, digit)), [global]);
|
|
9
|
+
const regexp_identifier_exactly = createRegExp(anyOf(
|
|
10
|
+
// Only letters (no digits)
|
|
11
|
+
oneOrMore(letter),
|
|
12
|
+
// Digits at the end
|
|
13
|
+
oneOrMore(letter).and(oneOrMore(digit)),
|
|
14
|
+
// Digits in the middle (letters before and after)
|
|
15
|
+
oneOrMore(letter)
|
|
16
|
+
.and(oneOrMore(digit))
|
|
17
|
+
.and(oneOrMore(letter)),
|
|
18
|
+
// Only digits (no letters)
|
|
19
|
+
oneOrMore(digit))
|
|
20
|
+
.at.lineStart()
|
|
21
|
+
.at.lineEnd());
|
|
22
|
+
function is_secret_line(line) {
|
|
23
|
+
if (line.endsWith(" # secret") || line.endsWith(" // secret")) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const NOT_SECRET_LINE_INCLUDES = ["http://", "https://"];
|
|
29
|
+
function is_not_secret_line(line) {
|
|
30
|
+
if (line.endsWith(" not secret")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
for (const not_secret_line_include of NOT_SECRET_LINE_INCLUDES) {
|
|
34
|
+
if (line.includes(not_secret_line_include)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const SECRET_WORD_REGEXPS = [
|
|
41
|
+
/^ghp_[A-Za-z0-9]{30}/, // GitHub Personal Access Token
|
|
42
|
+
/^glpat-[A-Za-z0-9]{20}/, // GitLab Personal Access Token
|
|
43
|
+
/^sk-[A-Za-z0-9-]{30}/, // Secret Key
|
|
44
|
+
/^sk_[A-Za-z0-9]{30}/, // Secret Key
|
|
45
|
+
/^sk_test_[A-Za-z0-9]{30}/, // Secret Key (test)
|
|
46
|
+
/^whsec_[A-Za-z0-9]{30}/, // WebHook Secret key
|
|
47
|
+
/^xox[a-z]-[A-Za-z0-9-]{27}/, // Slack Access Token
|
|
48
|
+
];
|
|
49
|
+
function is_secret_word(word) {
|
|
50
|
+
for (const secret_word_regexp of SECRET_WORD_REGEXPS) {
|
|
51
|
+
if (secret_word_regexp.test(word)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
async function is_secret_segment(segment, not_secret_segments, interactive) {
|
|
58
|
+
if (not_secret_segments.has(segment)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (regexp_identifier_exactly.test(segment)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (segment.length < 20) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (interactive) {
|
|
68
|
+
const confirmed_is_secret = await tui_confirm({
|
|
69
|
+
question: `Is ${tui_quote_smart_single(segment)} a secret?`,
|
|
70
|
+
default: false,
|
|
71
|
+
style_message: ansi_yellow,
|
|
72
|
+
});
|
|
73
|
+
if (!confirmed_is_secret) {
|
|
74
|
+
not_secret_segments.add(segment);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
export async function secret_check({ text, interactive }) {
|
|
81
|
+
const not_secret_segments = new Set();
|
|
82
|
+
const lines = text_split_lines(text);
|
|
83
|
+
for (const line of lines.toReversed()) {
|
|
84
|
+
if (is_secret_line(line)) {
|
|
85
|
+
throw new Error(`Secret detected: ${tui_quote_smart_single(line.trim())}`);
|
|
86
|
+
}
|
|
87
|
+
const words = line.match(regexp_word_global);
|
|
88
|
+
const segments = line.match(regexp_segment_global);
|
|
89
|
+
if (!words || !segments) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (is_not_secret_line(line)) {
|
|
93
|
+
for (const segment of segments) {
|
|
94
|
+
not_secret_segments.add(segment);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const word of words) {
|
|
99
|
+
if (is_secret_word(word)) {
|
|
100
|
+
throw new Error(`Secret detected: ${tui_quote_smart_single(word)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const segment of segments) {
|
|
104
|
+
if (await is_secret_segment(segment, not_secret_segments, interactive)) {
|
|
105
|
+
throw new Error(`Secret detected: ${tui_quote_smart_single(segment)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LF } from "./lib_char_control.js";
|
|
2
|
+
import { EMPTY } from "./lib_char_empty.js";
|
|
3
|
+
export function text_split_lines(text) {
|
|
4
|
+
const lines = text.split(/\r?\n/);
|
|
5
|
+
if (lines.length > 0) {
|
|
6
|
+
const last_line = lines.at(-1);
|
|
7
|
+
if (last_line === EMPTY) {
|
|
8
|
+
return lines.slice(0, -1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
export function text_join_lines(lines) {
|
|
14
|
+
return lines.length > 0 ? lines.join(LF) + LF : EMPTY;
|
|
15
|
+
}
|
|
16
|
+
function text_lines_matching_generic(text, pattern, remove) {
|
|
17
|
+
const regex = new RegExp(pattern);
|
|
18
|
+
const lines = text_split_lines(text);
|
|
19
|
+
const new_lines = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const found = regex.test(line);
|
|
22
|
+
if (found !== remove) {
|
|
23
|
+
new_lines.push(line);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return text_join_lines(new_lines);
|
|
27
|
+
}
|
|
28
|
+
export function text_lines_matching_only(text, pattern) {
|
|
29
|
+
return text_lines_matching_generic(text, pattern, false);
|
|
30
|
+
}
|
|
31
|
+
export function text_lines_matching_remove(text, pattern) {
|
|
32
|
+
return text_lines_matching_generic(text, pattern, true);
|
|
33
|
+
}
|
|
34
|
+
export function text_get_head(text, lines) {
|
|
35
|
+
return text_join_lines(text_split_lines(text).slice(0, lines));
|
|
36
|
+
}
|
|
37
|
+
export function text_get_tail(text, lines) {
|
|
38
|
+
return text_join_lines(text_split_lines(text).slice(-lines));
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { input as inquirer_input } from "@inquirer/prompts";
|
|
2
|
+
export async function tui_confirm({ question, default: default_value, style_message, }) {
|
|
3
|
+
const result = await inquirer_input({
|
|
4
|
+
message: question,
|
|
5
|
+
default: default_value === undefined ? undefined : default_value ? "Yes" : "No",
|
|
6
|
+
validate: (text) => {
|
|
7
|
+
const cleaned = text.trim().toLowerCase();
|
|
8
|
+
if (cleaned === "y" || cleaned === "yes" || cleaned === "n" || cleaned === "no") {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return "Please enter Yes or No";
|
|
12
|
+
},
|
|
13
|
+
transformer: (text, { isFinal: is_final }) => {
|
|
14
|
+
const cleaned = text.trim().toLowerCase();
|
|
15
|
+
return is_final ? (cleaned === "y" || cleaned === "yes" ? "Yes" : "No") : text;
|
|
16
|
+
},
|
|
17
|
+
theme: {
|
|
18
|
+
prefix: { idle: undefined, done: undefined },
|
|
19
|
+
style: {
|
|
20
|
+
message: style_message,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return result.trim().toLowerCase() === "y" || result.trim().toLowerCase() === "yes";
|
|
25
|
+
}
|
package/dist/src/lib_tui_none.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { EMPTY } from "./lib_char_empty.js";
|
|
2
2
|
import { DASH } from "./lib_char_punctuation.js";
|
|
3
3
|
// eslint-disable-next-line sonarjs/use-type-alias
|
|
4
|
-
function tui_none_generic(str,
|
|
5
|
-
|
|
6
|
-
return replacement;
|
|
7
|
-
}
|
|
8
|
-
return str.toString();
|
|
4
|
+
function tui_none_generic({ str, none }) {
|
|
5
|
+
return str === undefined || str === null || str === EMPTY ? none : str.toString();
|
|
9
6
|
}
|
|
10
7
|
export function tui_none_blank(str) {
|
|
11
|
-
return tui_none_generic(str, EMPTY);
|
|
8
|
+
return tui_none_generic({ str, none: EMPTY });
|
|
12
9
|
}
|
|
13
10
|
export function tui_none_dash(str) {
|
|
14
|
-
return tui_none_generic(str, DASH);
|
|
11
|
+
return tui_none_generic({ str, none: DASH });
|
|
15
12
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EMPTY } from "./lib_char_empty.js";
|
|
2
2
|
import { DOT } from "./lib_char_punctuation.js";
|
|
3
3
|
import { tui_justify_left, tui_justify_zero } from "./lib_tui_justify.js";
|
|
4
|
-
export function tui_number_plain({ num, justify_left, justify_right, }) {
|
|
5
|
-
let str = num === null || num === undefined ?
|
|
4
|
+
export function tui_number_plain({ num, none = EMPTY, justify_left, justify_right, }) {
|
|
5
|
+
let str = num === null || num === undefined ? none : num.toString();
|
|
6
6
|
if (justify_left !== undefined) {
|
|
7
7
|
str = tui_justify_left(justify_left, str);
|
|
8
8
|
}
|
|
@@ -4,8 +4,8 @@ import { ansi_bold } from "./lib_ansi.js";
|
|
|
4
4
|
export class TuiTable {
|
|
5
5
|
table;
|
|
6
6
|
columns_total;
|
|
7
|
-
constructor({ headings, alignments }) {
|
|
8
|
-
const constructor_options = { style: { head: [] } };
|
|
7
|
+
constructor({ headings, alignments, compact, }) {
|
|
8
|
+
const constructor_options = { style: { head: [], compact } };
|
|
9
9
|
constructor_options.head = headings.map((heading) => ansi_bold(heading));
|
|
10
10
|
this.columns_total = headings.length;
|
|
11
11
|
if (alignments) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnowennixon/diffdash",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A command-line tool to generate Git commit messages using AI",
|
|
5
5
|
"license": "0BSD",
|
|
6
6
|
"author": "John Owen Nixon",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/johnowennixon/diffdash.git"
|
|
9
|
+
"url": "git+https://github.com/johnowennixon/diffdash.git"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=20"
|
|
@@ -19,33 +19,35 @@
|
|
|
19
19
|
"diffdash": "dist/src/diffdash.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@ai-sdk/anthropic": "2.0.
|
|
23
|
-
"@ai-sdk/deepseek": "1.0.
|
|
24
|
-
"@ai-sdk/google": "2.0.
|
|
25
|
-
"@ai-sdk/openai": "2.0.
|
|
26
|
-
"@
|
|
27
|
-
"ai": "
|
|
28
|
-
"
|
|
22
|
+
"@ai-sdk/anthropic": "2.0.23",
|
|
23
|
+
"@ai-sdk/deepseek": "1.0.20",
|
|
24
|
+
"@ai-sdk/google": "2.0.17",
|
|
25
|
+
"@ai-sdk/openai": "2.0.42",
|
|
26
|
+
"@inquirer/prompts": "7.8.6",
|
|
27
|
+
"@openrouter/ai-sdk-provider": "1.2.0",
|
|
28
|
+
"ai": "5.0.60",
|
|
29
|
+
"ansis": "4.2.0",
|
|
29
30
|
"argparse": "2.0.1",
|
|
30
31
|
"cli-table3": "0.6.5",
|
|
31
32
|
"json5": "2.2.3",
|
|
33
|
+
"magic-regexp": "0.10.0",
|
|
32
34
|
"simple-git": "3.28.0",
|
|
33
|
-
"zod": "4.
|
|
35
|
+
"zod": "4.1.11"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
|
-
"@biomejs/biome": "2.
|
|
37
|
-
"@candide/tsgolint": "1.
|
|
38
|
+
"@biomejs/biome": "2.2.5",
|
|
39
|
+
"@candide/tsgolint": "1.4.0",
|
|
38
40
|
"@johnowennixon/add-shebangs": "1.1.0",
|
|
39
|
-
"@johnowennixon/chmodx": "2.
|
|
41
|
+
"@johnowennixon/chmodx": "2.1.0",
|
|
40
42
|
"@types/argparse": "2.0.17",
|
|
41
|
-
"@types/node": "24.2
|
|
42
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
43
|
-
"knip": "5.
|
|
43
|
+
"@types/node": "24.5.2",
|
|
44
|
+
"@typescript/native-preview": "7.0.0-dev.20250925.1",
|
|
45
|
+
"knip": "5.63.1",
|
|
44
46
|
"markdownlint-cli2": "0.18.1",
|
|
45
47
|
"npm-run-all2": "8.0.4",
|
|
46
|
-
"oxlint": "1.
|
|
48
|
+
"oxlint": "1.19.0",
|
|
47
49
|
"rimraf": "6.0.1",
|
|
48
|
-
"typescript": "5.9.
|
|
50
|
+
"typescript": "5.9.3"
|
|
49
51
|
},
|
|
50
52
|
"scripts": {
|
|
51
53
|
"build": "run-s -ls build:clean build:tsc build:shebang build:chmod",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
import { ansi_blue, ansi_bold } from "./lib_ansi.js";
|
|
3
|
-
export async function tui_readline_confirm(message) {
|
|
4
|
-
const query = ansi_bold(ansi_blue(`${message} [Y/n] `));
|
|
5
|
-
const rl = createInterface({
|
|
6
|
-
input: process.stdin,
|
|
7
|
-
output: process.stdout,
|
|
8
|
-
});
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
rl.question(query, (answer) => {
|
|
11
|
-
rl.close();
|
|
12
|
-
const normalized_answer = answer.trim().toLowerCase();
|
|
13
|
-
resolve(normalized_answer === "" || normalized_answer === "y" || normalized_answer === "yes");
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
}
|