@johnowennixon/diffdash 1.1.0 → 1.2.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/dist/package.json +80 -0
- package/dist/src/diffdash.js +26 -0
- package/dist/src/lib_abort.js +21 -0
- package/dist/src/lib_ansi.js +21 -0
- package/dist/src/lib_char_box.js +3 -0
- package/dist/src/lib_char_control.js +8 -0
- package/dist/src/lib_char_digit.js +10 -0
- package/dist/src/lib_char_empty.js +1 -0
- package/dist/src/lib_char_punctuation.js +37 -0
- package/dist/src/lib_cli.js +187 -0
- package/dist/src/lib_datetime.js +62 -0
- package/dist/src/lib_debug.js +68 -0
- package/dist/src/lib_diffdash_add.js +23 -0
- package/dist/src/lib_diffdash_cli.js +34 -0
- package/dist/src/lib_diffdash_config.js +52 -0
- package/dist/src/lib_diffdash_llm.js +24 -0
- package/dist/src/lib_diffdash_sequence.js +199 -0
- package/dist/src/lib_duration.js +29 -0
- package/dist/src/lib_enabled.js +30 -0
- package/dist/src/lib_env.js +18 -0
- package/dist/src/lib_error.js +14 -0
- package/dist/src/lib_file_path.js +22 -0
- package/dist/src/lib_git_message_display.js +4 -0
- package/dist/src/lib_git_message_generate.js +55 -0
- package/dist/src/lib_git_message_prompt.js +72 -0
- package/dist/src/lib_git_message_schema.js +16 -0
- package/dist/src/lib_git_message_validate.js +61 -0
- package/dist/src/lib_git_simple_open.js +24 -0
- package/dist/src/lib_git_simple_staging.js +41 -0
- package/dist/src/lib_inspect.js +4 -0
- package/dist/src/lib_llm_access.js +69 -0
- package/dist/src/lib_llm_chat.js +66 -0
- package/dist/src/lib_llm_config.js +23 -0
- package/dist/src/lib_llm_list.js +21 -0
- package/dist/src/lib_llm_model.js +336 -0
- package/dist/src/lib_llm_provider.js +63 -0
- package/dist/src/lib_llm_tokens.js +14 -0
- package/dist/src/lib_package.js +7 -0
- package/dist/src/lib_parse_number.js +13 -0
- package/dist/src/lib_stdio_write.js +14 -0
- package/dist/src/lib_string_types.js +1 -0
- package/dist/src/lib_tell.js +58 -0
- package/dist/src/lib_tui_block.js +10 -0
- package/dist/src/lib_tui_justify.js +29 -0
- package/dist/src/lib_tui_readline.js +16 -0
- package/dist/src/lib_tui_table.js +20 -0
- package/dist/src/lib_tui_truncate.js +13 -0
- package/dist/src/lib_type_infer.js +1 -0
- package/package.json +30 -24
- package/out/diffdash.cjs +0 -32581
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { abort_with_error, abort_with_warning } from "./lib_abort.js";
|
|
2
|
+
import { debug_channels, debug_inspect } from "./lib_debug.js";
|
|
3
|
+
import { diffdash_add_footer, diffdash_add_prefix_or_suffix } from "./lib_diffdash_add.js";
|
|
4
|
+
import { error_get_text } from "./lib_error.js";
|
|
5
|
+
import { git_message_display } from "./lib_git_message_display.js";
|
|
6
|
+
import { git_message_generate_result } from "./lib_git_message_generate.js";
|
|
7
|
+
import { git_message_validate_check, git_message_validate_get_result } from "./lib_git_message_validate.js";
|
|
8
|
+
import { git_simple_open_check_not_bare, git_simple_open_git_repo } from "./lib_git_simple_open.js";
|
|
9
|
+
import { git_simple_staging_create_commit, git_simple_staging_get_staged_diff, git_simple_staging_get_staged_diffstat, git_simple_staging_has_staged_changes, git_simple_staging_has_unstaged_changes, git_simple_staging_push_to_remote, git_simple_staging_stage_all_changes, } from "./lib_git_simple_staging.js";
|
|
10
|
+
import { llm_config_get_model_via } from "./lib_llm_config.js";
|
|
11
|
+
import { stdio_write_stdout_linefeed } from "./lib_stdio_write.js";
|
|
12
|
+
import { tell_action, tell_info, tell_plain, tell_success, tell_warning } from "./lib_tell.js";
|
|
13
|
+
import { tui_justify_left } from "./lib_tui_justify.js";
|
|
14
|
+
import { tui_readline_confirm } from "./lib_tui_readline.js";
|
|
15
|
+
async function phase_open() {
|
|
16
|
+
const git = await git_simple_open_git_repo();
|
|
17
|
+
await git_simple_open_check_not_bare(git);
|
|
18
|
+
return git;
|
|
19
|
+
}
|
|
20
|
+
async function phase_add({ config, git }) {
|
|
21
|
+
const { auto_add, disable_add, silent } = config;
|
|
22
|
+
if (debug_channels.git) {
|
|
23
|
+
const status = await git.status();
|
|
24
|
+
debug_inspect(status, "status");
|
|
25
|
+
}
|
|
26
|
+
const has_staged_changes = await git_simple_staging_has_staged_changes(git);
|
|
27
|
+
if (has_staged_changes) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const has_unstaged_changes = await git_simple_staging_has_unstaged_changes(git);
|
|
31
|
+
if (!has_unstaged_changes) {
|
|
32
|
+
abort_with_warning("No changes found in the repository - there is nothing to commit");
|
|
33
|
+
}
|
|
34
|
+
if (disable_add) {
|
|
35
|
+
abort_with_warning("No staged changes found and adding changes is disabled");
|
|
36
|
+
}
|
|
37
|
+
if (auto_add) {
|
|
38
|
+
if (!silent) {
|
|
39
|
+
tell_action("Auto-adding changes");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const add_confirmed = await tui_readline_confirm("No staged changes found - would you like to add all changes?");
|
|
44
|
+
if (!add_confirmed) {
|
|
45
|
+
abort_with_warning("Please add changes before creating a commit");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await git_simple_staging_stage_all_changes(git);
|
|
49
|
+
if (!silent) {
|
|
50
|
+
tell_success("All changed files added successfully");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function phase_status({ config, git }) {
|
|
54
|
+
const { disable_status, silent } = config;
|
|
55
|
+
if (disable_status || silent) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
tell_info("Files staged for commit:");
|
|
59
|
+
const status = await git.status();
|
|
60
|
+
const files_added = status.files.filter((file) => file.index === "A");
|
|
61
|
+
const files_deleted = status.files.filter((file) => file.index === "D");
|
|
62
|
+
const files_renamed = status.files.filter((file) => file.index === "R");
|
|
63
|
+
const files_modified = status.files.filter((file) => file.index === "M");
|
|
64
|
+
const files_staged = [
|
|
65
|
+
// All the possible staged index codes
|
|
66
|
+
...files_added,
|
|
67
|
+
...files_deleted,
|
|
68
|
+
...files_renamed,
|
|
69
|
+
...files_modified,
|
|
70
|
+
];
|
|
71
|
+
const max_length = Math.max(...files_staged.map((file) => file.path.length), 10);
|
|
72
|
+
for (const file of files_added) {
|
|
73
|
+
stdio_write_stdout_linefeed(` ${tui_justify_left(max_length, file.path)} (added)`);
|
|
74
|
+
}
|
|
75
|
+
for (const file of files_modified) {
|
|
76
|
+
stdio_write_stdout_linefeed(` ${tui_justify_left(max_length, file.path)} (modified)`);
|
|
77
|
+
}
|
|
78
|
+
for (const file of files_renamed) {
|
|
79
|
+
stdio_write_stdout_linefeed(` ${tui_justify_left(max_length, file.path)} (renamed from ${file.from})`);
|
|
80
|
+
}
|
|
81
|
+
for (const file of files_deleted) {
|
|
82
|
+
stdio_write_stdout_linefeed(` ${tui_justify_left(max_length, file.path)} (deleted)`);
|
|
83
|
+
}
|
|
84
|
+
if (files_staged.length === 0) {
|
|
85
|
+
abort_with_warning("No files staged for commit");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function phase_compare({ config, git }) {
|
|
89
|
+
const { silent } = config;
|
|
90
|
+
if (!silent) {
|
|
91
|
+
tell_action("Generating Git commit messages using all models in parallel");
|
|
92
|
+
}
|
|
93
|
+
const { all_llm_configs, add_prefix, add_suffix } = config;
|
|
94
|
+
const diffstat = await git_simple_staging_get_staged_diffstat(git);
|
|
95
|
+
const diff = await git_simple_staging_get_staged_diff(git);
|
|
96
|
+
const inputs = { diffstat, diff };
|
|
97
|
+
const result_promises = all_llm_configs.map((llm_config) => git_message_generate_result({ llm_config, inputs }));
|
|
98
|
+
const all_results = await Promise.all(result_promises);
|
|
99
|
+
for (const result of all_results) {
|
|
100
|
+
const { llm_config, seconds, error_text } = result;
|
|
101
|
+
let { git_message } = result;
|
|
102
|
+
const model_via = llm_config_get_model_via(llm_config);
|
|
103
|
+
if (error_text) {
|
|
104
|
+
tell_warning(`Failed to generate a commit message in ${seconds} seconds using ${model_via}: ${error_text}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!git_message) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
tell_info(`Git commit message in ${seconds} seconds using ${model_via}:`);
|
|
111
|
+
const validation_result = git_message_validate_get_result(git_message);
|
|
112
|
+
const teller = validation_result.valid ? tell_plain : tell_warning;
|
|
113
|
+
git_message = diffdash_add_prefix_or_suffix({ git_message, add_prefix, add_suffix });
|
|
114
|
+
git_message = diffdash_add_footer({ git_message, llm_config });
|
|
115
|
+
git_message_display({ git_message, teller });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function phase_commit({ config, git }) {
|
|
119
|
+
const { add_prefix, add_suffix, auto_commit, disable_commit, disable_preview, silent, llm_config } = config;
|
|
120
|
+
const model_via = llm_config_get_model_via(llm_config);
|
|
121
|
+
if (!silent) {
|
|
122
|
+
tell_action(`Generating the Git commit message using ${model_via}`);
|
|
123
|
+
}
|
|
124
|
+
const diffstat = await git_simple_staging_get_staged_diffstat(git);
|
|
125
|
+
const diff = await git_simple_staging_get_staged_diff(git);
|
|
126
|
+
const inputs = { diffstat, diff };
|
|
127
|
+
const result = await git_message_generate_result({ llm_config, inputs });
|
|
128
|
+
const { error_text } = result;
|
|
129
|
+
let { git_message } = result;
|
|
130
|
+
if (error_text) {
|
|
131
|
+
abort_with_error(`Failed to generate a commit message using ${model_via}: ${error_text}`);
|
|
132
|
+
}
|
|
133
|
+
if (!git_message) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
git_message_validate_check(git_message);
|
|
137
|
+
git_message = diffdash_add_prefix_or_suffix({ git_message, add_prefix, add_suffix });
|
|
138
|
+
git_message = diffdash_add_footer({ git_message, llm_config });
|
|
139
|
+
if (!disable_preview && !silent) {
|
|
140
|
+
git_message_display({ git_message, teller: tell_plain });
|
|
141
|
+
}
|
|
142
|
+
if (disable_commit) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (auto_commit) {
|
|
146
|
+
if (!silent) {
|
|
147
|
+
tell_action("Auto-committing changes");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const commit_confirmed = await tui_readline_confirm("Do you want to commit these changes?");
|
|
152
|
+
if (!commit_confirmed) {
|
|
153
|
+
abort_with_warning("Commit cancelled by user");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await git_simple_staging_create_commit(git, git_message);
|
|
157
|
+
if (!silent) {
|
|
158
|
+
tell_success("Changes committed successfully");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function phase_push({ config, git }) {
|
|
162
|
+
const { auto_push, disable_commit, disable_push, push_no_verify, push_force, silent } = config;
|
|
163
|
+
if (disable_push || disable_commit) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (auto_push) {
|
|
167
|
+
if (!silent) {
|
|
168
|
+
tell_action("Auto-pushing changes");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const push_confirmed = await tui_readline_confirm("Do you want to push these changes?");
|
|
173
|
+
if (!push_confirmed) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await git_simple_staging_push_to_remote({ git, no_verify: push_no_verify, force: push_force });
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
abort_with_error(`Failed to push to remote: ${error_get_text(error)}`);
|
|
182
|
+
}
|
|
183
|
+
if (!silent) {
|
|
184
|
+
tell_success("Changes pushed successfully");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export async function diffdash_sequence_normal(config) {
|
|
188
|
+
const git = await phase_open();
|
|
189
|
+
await phase_add({ config, git });
|
|
190
|
+
await phase_status({ config, git });
|
|
191
|
+
await phase_commit({ config, git });
|
|
192
|
+
await phase_push({ config, git });
|
|
193
|
+
}
|
|
194
|
+
export async function diffdash_sequence_compare(config) {
|
|
195
|
+
const git = await phase_open();
|
|
196
|
+
await phase_add({ config, git });
|
|
197
|
+
await phase_status({ config, git });
|
|
198
|
+
await phase_compare({ config, git });
|
|
199
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
2
|
+
export class Duration {
|
|
3
|
+
hrtime_start;
|
|
4
|
+
hrtime_stop;
|
|
5
|
+
start() {
|
|
6
|
+
this.hrtime_start = process.hrtime.bigint();
|
|
7
|
+
}
|
|
8
|
+
stop() {
|
|
9
|
+
this.hrtime_stop = process.hrtime.bigint();
|
|
10
|
+
}
|
|
11
|
+
nanoseconds() {
|
|
12
|
+
if (this.hrtime_start === undefined || this.hrtime_stop === undefined) {
|
|
13
|
+
abort_with_error("Duration uninitialized");
|
|
14
|
+
}
|
|
15
|
+
return Number(this.hrtime_stop - this.hrtime_start);
|
|
16
|
+
}
|
|
17
|
+
seconds_complete() {
|
|
18
|
+
return this.nanoseconds() / 1_000_000_000;
|
|
19
|
+
}
|
|
20
|
+
milliseconds_complete() {
|
|
21
|
+
return this.nanoseconds() / 1_000_000;
|
|
22
|
+
}
|
|
23
|
+
seconds_rounded() {
|
|
24
|
+
return Math.round(this.seconds_complete());
|
|
25
|
+
}
|
|
26
|
+
milliseconds_rounded() {
|
|
27
|
+
return Math.round(this.milliseconds_complete());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DIGIT_0, DIGIT_1 } from "./lib_char_digit.js";
|
|
2
|
+
export function enabled_from_string(value, options) {
|
|
3
|
+
const fallback = options?.default ?? false;
|
|
4
|
+
if (value === undefined || value === null) {
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
if (value === DIGIT_0) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (value === DIGIT_1) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
const upper = value.toUpperCase();
|
|
14
|
+
if (upper === "FALSE") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (upper === "TRUE") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (upper === "DISABLED") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (upper === "ENABLED") {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
export function enabled_from_env(key, options) {
|
|
29
|
+
return enabled_from_string(process.env[key], options);
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
2
|
+
import { EMPTY } from "./lib_char_empty.js";
|
|
3
|
+
export const DOT_ENV = ".env";
|
|
4
|
+
export function env_get(key) {
|
|
5
|
+
return process.env[key] ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function env_get_substitute(key, substitute) {
|
|
8
|
+
return env_get(key) ?? substitute;
|
|
9
|
+
}
|
|
10
|
+
export function env_get_empty(key) {
|
|
11
|
+
return env_get_substitute(key, EMPTY);
|
|
12
|
+
}
|
|
13
|
+
export function env_get_abort(key) {
|
|
14
|
+
return env_get(key) ?? abort_with_error(`Unable to find environment key: ${key}`);
|
|
15
|
+
}
|
|
16
|
+
export function env_set(key, value) {
|
|
17
|
+
process.env[key] = value;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
2
|
+
export function error_ignore(_error) {
|
|
3
|
+
/* intentionally left empty */
|
|
4
|
+
}
|
|
5
|
+
export function error_console(error) {
|
|
6
|
+
console.error(error);
|
|
7
|
+
}
|
|
8
|
+
export function error_get_text(error) {
|
|
9
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
10
|
+
}
|
|
11
|
+
export function error_abort(error) {
|
|
12
|
+
const message = `Unhandled error: ${error_get_text(error)}`;
|
|
13
|
+
abort_with_error(message);
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function file_path_delimiter() {
|
|
3
|
+
return path.delimiter;
|
|
4
|
+
}
|
|
5
|
+
export function file_path_join(segment1, ...segments) {
|
|
6
|
+
return path.join(segment1, ...segments);
|
|
7
|
+
}
|
|
8
|
+
export function file_path_basename(file_path) {
|
|
9
|
+
return path.basename(file_path);
|
|
10
|
+
}
|
|
11
|
+
export function file_path_dirname(file_path) {
|
|
12
|
+
return path.dirname(file_path);
|
|
13
|
+
}
|
|
14
|
+
export function file_path_extname(file_path) {
|
|
15
|
+
return path.extname(file_path);
|
|
16
|
+
}
|
|
17
|
+
export function file_path_absolute(relative_path) {
|
|
18
|
+
return path.resolve(relative_path);
|
|
19
|
+
}
|
|
20
|
+
export function file_path_relative({ base_dir, file_path }) {
|
|
21
|
+
return path.relative(base_dir, file_path);
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Duration } from "./lib_duration.js";
|
|
2
|
+
import { error_get_text } from "./lib_error.js";
|
|
3
|
+
import { git_message_get_system_prompt, git_message_get_user_prompt } from "./lib_git_message_prompt.js";
|
|
4
|
+
import { git_message_schema, git_message_schema_format } from "./lib_git_message_schema.js";
|
|
5
|
+
import { llm_chat_generate_object, llm_chat_generate_text } from "./lib_llm_chat.js";
|
|
6
|
+
import { llm_tokens_count_estimated, llm_tokens_debug_usage } from "./lib_llm_tokens.js";
|
|
7
|
+
async function git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, }) {
|
|
8
|
+
const llm_response_text = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt });
|
|
9
|
+
return llm_response_text;
|
|
10
|
+
}
|
|
11
|
+
async function git_message_generate_structured({ llm_config, system_prompt, user_prompt, }) {
|
|
12
|
+
const schema = git_message_schema;
|
|
13
|
+
const llm_response_structured = await llm_chat_generate_object({
|
|
14
|
+
llm_config,
|
|
15
|
+
system_prompt,
|
|
16
|
+
user_prompt,
|
|
17
|
+
schema,
|
|
18
|
+
});
|
|
19
|
+
const llm_response_text = git_message_schema_format(llm_response_structured);
|
|
20
|
+
return llm_response_text;
|
|
21
|
+
}
|
|
22
|
+
export async function git_message_generate_string({ llm_config, inputs, }) {
|
|
23
|
+
const { context_window, has_structured_json } = llm_config.llm_model_detail;
|
|
24
|
+
const system_prompt = git_message_get_system_prompt({ has_structured_json });
|
|
25
|
+
// Estimate remaining prompt length
|
|
26
|
+
const user_tokens = context_window - llm_tokens_count_estimated({ llm_config, text: system_prompt }) - 1000;
|
|
27
|
+
const user_length = user_tokens * 3;
|
|
28
|
+
const user_prompt = git_message_get_user_prompt({
|
|
29
|
+
has_structured_json,
|
|
30
|
+
inputs,
|
|
31
|
+
max_length: user_length,
|
|
32
|
+
});
|
|
33
|
+
llm_tokens_debug_usage({ name: "Inputs", llm_config, text: system_prompt + user_prompt });
|
|
34
|
+
const llm_response_text = has_structured_json
|
|
35
|
+
? await git_message_generate_structured({ llm_config, system_prompt, user_prompt })
|
|
36
|
+
: await git_message_generate_unstructured({ llm_config, system_prompt, user_prompt });
|
|
37
|
+
llm_tokens_debug_usage({ name: "Outputs", llm_config, text: llm_response_text });
|
|
38
|
+
return llm_response_text;
|
|
39
|
+
}
|
|
40
|
+
export async function git_message_generate_result({ llm_config, inputs, }) {
|
|
41
|
+
const duration = new Duration();
|
|
42
|
+
duration.start();
|
|
43
|
+
try {
|
|
44
|
+
const git_message = await git_message_generate_string({ llm_config, inputs });
|
|
45
|
+
duration.stop();
|
|
46
|
+
const seconds = duration.seconds_rounded();
|
|
47
|
+
return { llm_config, seconds, git_message, error_text: null };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
duration.stop();
|
|
51
|
+
const seconds = duration.seconds_rounded();
|
|
52
|
+
const error_text = error_get_text(error);
|
|
53
|
+
return { llm_config, seconds, git_message: null, error_text };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LF } from "./lib_char_control.js";
|
|
2
|
+
import { EMPTY } from "./lib_char_empty.js";
|
|
3
|
+
const portion_role = `
|
|
4
|
+
Your role is to generate a Git commit message in conversational English.
|
|
5
|
+
The user does not want Conventional Commits - the summary line must be a normal sentence.
|
|
6
|
+
`.trim() + LF;
|
|
7
|
+
const portion_inputs = `
|
|
8
|
+
The user will send you a <diffstat> block, the output of a 'git diff --staged --stat' command.
|
|
9
|
+
The user will send you a <diff> block, the output of a 'git diff --staged' command.
|
|
10
|
+
`.trim() + LF;
|
|
11
|
+
const portion_reminders = `
|
|
12
|
+
Some reminders of how diffs work:
|
|
13
|
+
- Lines that start with a single plus sign have been added to the file.
|
|
14
|
+
- Lines that start with a single minus sign have been removed from the file.
|
|
15
|
+
- Lines that start with @@ indicate a jump to a different section of the file - you can not see the code in these gaps.
|
|
16
|
+
`.trim() + LF;
|
|
17
|
+
const portion_format_structured = `
|
|
18
|
+
You must output in the following format (this will be forced):
|
|
19
|
+
- summary_line: a single sentence giving a concise summary of the changes.
|
|
20
|
+
- extra_lines: additional sentences giving more information about the changes.
|
|
21
|
+
`.trim() + LF;
|
|
22
|
+
const portion_format_unstructured = `
|
|
23
|
+
You must output in the following format - without any preamble or conclusion:
|
|
24
|
+
- First line: a single sentence giving a concise summary of the changes.
|
|
25
|
+
- Second line: completely blank - not even any spaces.
|
|
26
|
+
- Then an unordered list (with a dash prefix) of additional sentences giving more information about the changes.
|
|
27
|
+
- And nothing else.
|
|
28
|
+
`.trim() + LF;
|
|
29
|
+
function portion_format(has_structured_json) {
|
|
30
|
+
return has_structured_json ? portion_format_structured : portion_format_unstructured;
|
|
31
|
+
}
|
|
32
|
+
const portion_instructions = `
|
|
33
|
+
Use the imperative mood and present tense.
|
|
34
|
+
Please write in full sentences that start with a capital letter.
|
|
35
|
+
Write prose rather than making lists.
|
|
36
|
+
Keep each sentence no longer than about twenty words.
|
|
37
|
+
Focus on why things were changed, not how or what.
|
|
38
|
+
Things that are being added are more relevant than things that are being deleted.
|
|
39
|
+
Code changes are more important than documentation changes.
|
|
40
|
+
Be humble - you don't really know why the change was made.
|
|
41
|
+
Don't assume the change is always an improvement - it might be making things worse.
|
|
42
|
+
The number of additional sentences should depend upon the complexity of the change.
|
|
43
|
+
A simple change needs only two additional sentences scaling up to a complex change with five additional sentences.
|
|
44
|
+
If there are a lot of changes, you will need to summarize even more.
|
|
45
|
+
`.trim() + LF;
|
|
46
|
+
const portion_final = `
|
|
47
|
+
Everything you write will be checked for validity and then saved directly to Git - it will not be reviewed by a human.
|
|
48
|
+
Therefore, you must just output the Git message itself without any introductory or concluding sections.
|
|
49
|
+
`.trim() + LF;
|
|
50
|
+
export function git_message_get_system_prompt({ has_structured_json }) {
|
|
51
|
+
let system_prompt = EMPTY;
|
|
52
|
+
system_prompt += portion_role + LF;
|
|
53
|
+
system_prompt += portion_inputs + LF;
|
|
54
|
+
system_prompt += portion_reminders + LF;
|
|
55
|
+
system_prompt += portion_format(has_structured_json) + LF;
|
|
56
|
+
system_prompt += portion_instructions + LF;
|
|
57
|
+
system_prompt += portion_final + LF;
|
|
58
|
+
return system_prompt.trim();
|
|
59
|
+
}
|
|
60
|
+
export function git_message_get_user_prompt({ has_structured_json, inputs, max_length, }) {
|
|
61
|
+
const { diffstat, diff } = inputs;
|
|
62
|
+
const truncate = diffstat.length + diff.length > max_length;
|
|
63
|
+
const diff_truncated = truncate ? diff.slice(0, max_length - diffstat.length) + LF : diff;
|
|
64
|
+
let user_prompt = EMPTY;
|
|
65
|
+
user_prompt += "<diffstat>" + LF + diffstat + "</diffstat>" + LF + LF;
|
|
66
|
+
user_prompt += "<diff>" + LF + diff_truncated + "</diff>" + LF + LF;
|
|
67
|
+
if (truncate) {
|
|
68
|
+
user_prompt += "Please note: the Diff above has been truncated" + LF + LF;
|
|
69
|
+
}
|
|
70
|
+
user_prompt += portion_format(has_structured_json) + LF;
|
|
71
|
+
return user_prompt.trim();
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LF } from "./lib_char_control.js";
|
|
3
|
+
import { EMPTY } from "./lib_char_empty.js";
|
|
4
|
+
export const git_message_schema = z.object({
|
|
5
|
+
summary_line: z.string().describe("A single sentence giving a concise summary of the changes."),
|
|
6
|
+
extra_lines: z
|
|
7
|
+
.array(z.string().describe("Another sentence giving more information about the changes."))
|
|
8
|
+
.describe("More information about the changes."),
|
|
9
|
+
});
|
|
10
|
+
export function git_message_schema_format(message) {
|
|
11
|
+
return [
|
|
12
|
+
message.summary_line,
|
|
13
|
+
EMPTY, // Empty line
|
|
14
|
+
...message.extra_lines.map((line) => (line.startsWith("- ") ? line : `- ${line}`)),
|
|
15
|
+
].join(LF);
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
2
|
+
import { LF } from "./lib_char_control.js";
|
|
3
|
+
import { EMPTY } from "./lib_char_empty.js";
|
|
4
|
+
import { ASTERISK, DASH, SPACE } from "./lib_char_punctuation.js";
|
|
5
|
+
import { git_message_display } from "./lib_git_message_display.js";
|
|
6
|
+
import { tell_warning } from "./lib_tell.js";
|
|
7
|
+
const DEFAULT_MIN_LENGTH = 40;
|
|
8
|
+
const DEFAULT_MAX_LENGTH = 4000;
|
|
9
|
+
export function git_message_validate_get_result(git_message) {
|
|
10
|
+
const min_length = DEFAULT_MIN_LENGTH;
|
|
11
|
+
const max_length = DEFAULT_MAX_LENGTH;
|
|
12
|
+
if (!git_message || git_message.trim() === EMPTY) {
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
reason: "message is empty",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (git_message.length < min_length) {
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
reason: `too short (minimum ${min_length} characters)`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (git_message.length > max_length) {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
reason: `too long (maximum ${max_length} characters)`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const lines = git_message.trim().split(LF);
|
|
31
|
+
if (lines.length < 3) {
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
reason: "need at least 3 lines",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (lines[1] && lines[1].trim() !== EMPTY) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
reason: "missing blank line after summary line",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
for (const line of lines.slice(2)) {
|
|
44
|
+
if (!line.startsWith(DASH + SPACE) && !line.startsWith(ASTERISK + SPACE)) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
reason: "bullet points are malformed",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
valid: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function git_message_validate_check(git_message) {
|
|
56
|
+
const validation_result = git_message_validate_get_result(git_message);
|
|
57
|
+
if (!validation_result.valid) {
|
|
58
|
+
git_message_display({ git_message, teller: tell_warning });
|
|
59
|
+
abort_with_error(`Generated commit message failed validation: ${validation_result.reason}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { simpleGit } from "simple-git";
|
|
2
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
3
|
+
import { file_path_absolute } from "./lib_file_path.js";
|
|
4
|
+
export async function git_simple_open_git_repo(repo_path = process.cwd()) {
|
|
5
|
+
const resolved_path = file_path_absolute(repo_path);
|
|
6
|
+
const git = simpleGit(resolved_path);
|
|
7
|
+
const is_repo = await git.checkIsRepo();
|
|
8
|
+
if (!is_repo) {
|
|
9
|
+
abort_with_error("This directory is not in a git repository");
|
|
10
|
+
}
|
|
11
|
+
return git;
|
|
12
|
+
}
|
|
13
|
+
export async function git_simple_open_check_not_bare(git) {
|
|
14
|
+
const is_bare_repository = await git.raw(["rev-parse", "--is-bare-repository"]);
|
|
15
|
+
if (is_bare_repository === "true") {
|
|
16
|
+
abort_with_error("Cannot operate on a bare repository");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function git_simple_open_check_no_conflicts(git) {
|
|
20
|
+
const status = await git.status();
|
|
21
|
+
if (status.conflicted.length > 0) {
|
|
22
|
+
abort_with_error("Cannot operate on a repository with conflicts");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SPACE } from "./lib_char_punctuation.js";
|
|
2
|
+
export async function git_simple_staging_has_staged_changes(git) {
|
|
3
|
+
const status = await git.status();
|
|
4
|
+
for (const file of status.files) {
|
|
5
|
+
if ("ADMR".includes(file.index)) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
export async function git_simple_staging_has_unstaged_changes(git) {
|
|
12
|
+
const status = await git.status();
|
|
13
|
+
for (const file of status.files) {
|
|
14
|
+
if (file.working_dir !== SPACE) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
export async function git_simple_staging_stage_all_changes(git) {
|
|
21
|
+
await git.add(["--all"]);
|
|
22
|
+
}
|
|
23
|
+
export async function git_simple_staging_get_staged_diffstat(git) {
|
|
24
|
+
return await git.diff(["--cached", "--stat"]);
|
|
25
|
+
}
|
|
26
|
+
export async function git_simple_staging_get_staged_diff(git) {
|
|
27
|
+
return await git.diff(["--cached"]);
|
|
28
|
+
}
|
|
29
|
+
export async function git_simple_staging_create_commit(git, git_message) {
|
|
30
|
+
await git.commit(git_message);
|
|
31
|
+
}
|
|
32
|
+
export async function git_simple_staging_push_to_remote({ git, no_verify = false, force = false, }) {
|
|
33
|
+
const push_args = ["--follow-tags"];
|
|
34
|
+
if (no_verify) {
|
|
35
|
+
push_args.push("--no-verify");
|
|
36
|
+
}
|
|
37
|
+
if (force) {
|
|
38
|
+
push_args.push("--force");
|
|
39
|
+
}
|
|
40
|
+
await git.push(push_args);
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { abort_with_error } from "./lib_abort.js";
|
|
2
|
+
import { COMMA } from "./lib_char_punctuation.js";
|
|
3
|
+
import { llm_model_find_detail } from "./lib_llm_model.js";
|
|
4
|
+
import { llm_provider_get_api_key, llm_provider_get_api_key_env } from "./lib_llm_provider.js";
|
|
5
|
+
export function llm_access_available({ llm_model_details, llm_model_name, llm_excludes, }) {
|
|
6
|
+
if (llm_excludes) {
|
|
7
|
+
const llm_excludes_array = llm_excludes.split(COMMA).map((exclude) => exclude.trim());
|
|
8
|
+
for (const llm_exclude of llm_excludes_array) {
|
|
9
|
+
if (llm_model_name.includes(llm_exclude)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const detail = llm_model_find_detail({ llm_model_details, llm_model_name });
|
|
15
|
+
const { llm_provider, llm_model_code_direct, llm_model_code_requesty, llm_model_code_openrouter } = detail;
|
|
16
|
+
if (llm_model_code_direct !== null && llm_provider !== null) {
|
|
17
|
+
if (llm_provider_get_api_key(llm_provider)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (llm_model_code_requesty !== null) {
|
|
22
|
+
if (llm_provider_get_api_key("requesty")) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (llm_model_code_openrouter !== null) {
|
|
27
|
+
if (llm_provider_get_api_key("openrouter")) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export function llm_access_get({ llm_model_details, llm_model_name, llm_router, }) {
|
|
34
|
+
const detail = llm_model_find_detail({ llm_model_details, llm_model_name });
|
|
35
|
+
const { llm_provider, llm_model_code_direct, llm_model_code_requesty, llm_model_code_openrouter } = detail;
|
|
36
|
+
if (!llm_router) {
|
|
37
|
+
if (llm_model_code_direct !== null && llm_provider !== null) {
|
|
38
|
+
const llm_api_key = llm_provider_get_api_key(llm_provider);
|
|
39
|
+
if (llm_api_key) {
|
|
40
|
+
return { llm_model_code: llm_model_code_direct, llm_provider, llm_api_key };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (llm_model_code_requesty !== null) {
|
|
45
|
+
const llm_api_key = llm_provider_get_api_key("requesty");
|
|
46
|
+
if (llm_api_key) {
|
|
47
|
+
return { llm_model_code: llm_model_code_requesty, llm_provider: "requesty", llm_api_key };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (llm_model_code_openrouter !== null) {
|
|
51
|
+
const llm_api_key = llm_provider_get_api_key("openrouter");
|
|
52
|
+
if (llm_api_key) {
|
|
53
|
+
return { llm_model_code: llm_model_code_openrouter, llm_provider: "openrouter", llm_api_key };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (llm_model_code_direct !== null && llm_provider !== null) {
|
|
57
|
+
const llm_api_key = llm_provider_get_api_key(llm_provider);
|
|
58
|
+
if (llm_api_key) {
|
|
59
|
+
return { llm_model_code: llm_model_code_direct, llm_provider, llm_api_key };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const env_requesty = llm_provider_get_api_key_env("requesty");
|
|
63
|
+
const env_openrouter = llm_provider_get_api_key_env("openrouter");
|
|
64
|
+
if (llm_provider !== null) {
|
|
65
|
+
const env_provider = llm_provider_get_api_key_env(llm_provider);
|
|
66
|
+
abort_with_error(`Please set environment variable ${env_requesty}, ${env_openrouter} or ${env_provider}`);
|
|
67
|
+
}
|
|
68
|
+
abort_with_error(`Please set environment variable ${env_requesty} or ${env_openrouter}`);
|
|
69
|
+
}
|