gityo 1.0.10 → 1.0.12
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 +62 -6
- package/dist/index.js +272 -27
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -46,13 +46,14 @@ If you already staged files before running `gityo`, only those are used for the
|
|
|
46
46
|
gityo
|
|
47
47
|
gityo --generate
|
|
48
48
|
gityo --model fast --generate
|
|
49
|
-
gityo --
|
|
49
|
+
gityo --style concise --generate
|
|
50
|
+
gityo --input "fix login redirect bug"
|
|
50
51
|
gityo --yolo
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
## AI setup
|
|
54
55
|
|
|
55
|
-
A configured model is required — gityo won't run without one, and the API key must be resolvable from your environment even when you pass `--
|
|
56
|
+
A configured model is required — gityo won't run without one, and the API key must be resolvable from your environment even when you pass `--input`. Models live in a `models` map in your config file. Each key is a name you can pick with `--model`; the `default` key is used when you don't pass `--model`:
|
|
56
57
|
|
|
57
58
|
```json
|
|
58
59
|
{
|
|
@@ -68,7 +69,8 @@ A configured model is required — gityo won't run without one, and the API key
|
|
|
68
69
|
"apiKeyEnv": "OPENAI_API_KEY",
|
|
69
70
|
"model": "gpt-5-mini"
|
|
70
71
|
}
|
|
71
|
-
}
|
|
72
|
+
},
|
|
73
|
+
"style": "concise"
|
|
72
74
|
}
|
|
73
75
|
```
|
|
74
76
|
|
|
@@ -101,6 +103,7 @@ Then use:
|
|
|
101
103
|
```bash
|
|
102
104
|
gityo --generate
|
|
103
105
|
gityo --model fast --generate
|
|
106
|
+
gityo --style concise --generate
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
Providers are the official Vercel AI SDK packages: `@ai-sdk/openai`, `@ai-sdk/openai-compatible`, `@ai-sdk/anthropic`, `@ai-sdk/google`, `@ai-sdk/xai`, `@ai-sdk/azure`, `@ai-sdk/amazon-bedrock`, `@ai-sdk/groq`, `@ai-sdk/mistral`, `@ai-sdk/deepseek`, `@ai-sdk/togetherai`, `@ai-sdk/fireworks`, `@ai-sdk/perplexity`, `@ai-sdk/cohere`, `@ai-sdk/cerebras`, `@ai-sdk/luma`, `@ai-sdk/fal`, `@ai-sdk/deepinfra`, `@ai-sdk/google-vertex`, `@openrouter/ai-sdk-provider`, plus `ai-sdk-ollama`, `ollama-ai-provider-v2`, `workers-ai-provider`, `zhipu-ai-provider`, `sambanova-ai-provider`, `vercel-minimax-ai-provider`, `@aihubmix/ai-sdk-provider`, `ai-gateway-provider`, `@friendliai/ai-provider`, `@helicone/ai-sdk-provider`, and `ai-sdk-provider-opencode-sdk`.
|
|
@@ -116,7 +119,7 @@ gityo config
|
|
|
116
119
|
Config is edited by hand in a JSON file. Project config goes in:
|
|
117
120
|
|
|
118
121
|
```text
|
|
119
|
-
.gityo.
|
|
122
|
+
.gityo.json
|
|
120
123
|
```
|
|
121
124
|
|
|
122
125
|
Global config goes in:
|
|
@@ -151,13 +154,66 @@ Example:
|
|
|
151
154
|
"model": "gpt-5-nano"
|
|
152
155
|
}
|
|
153
156
|
},
|
|
157
|
+
"style": "concise",
|
|
158
|
+
"styles": {
|
|
159
|
+
"team": "Use conventional commits. Keep the subject under 72 characters.",
|
|
160
|
+
"release": {
|
|
161
|
+
"path": ".gityo/release-style.md"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
154
164
|
"autoAcceptMessage": false,
|
|
155
165
|
"postCommand": "push",
|
|
156
166
|
"autoRunPostCommand": false,
|
|
157
|
-
"instructions": "Write short, clear commit messages."
|
|
167
|
+
"instructions": "Write short, clear commit messages.",
|
|
168
|
+
"maxDiffTokens": 24000,
|
|
169
|
+
"perFileCap": 400
|
|
158
170
|
}
|
|
159
171
|
```
|
|
160
172
|
|
|
173
|
+
### Commit message styles
|
|
174
|
+
|
|
175
|
+
Styles control the base commit-message convention sent to the model. Built-in
|
|
176
|
+
styles are `default`, `concise`, `explanatory`, `plain`, and `gitmoji`.
|
|
177
|
+
|
|
178
|
+
- `default` uses conventional commits and adds a body only for substantial,
|
|
179
|
+
multi-part changes that benefit from more context.
|
|
180
|
+
- `concise` uses conventional commits and adds a body only when it is essential.
|
|
181
|
+
- `explanatory` uses conventional commits and encourages a useful explanatory
|
|
182
|
+
body.
|
|
183
|
+
- `plain` produces a short, non-conventional imperative subject line.
|
|
184
|
+
- `gitmoji` prefixes a conventional subject with a relevant gitmoji.
|
|
185
|
+
|
|
186
|
+
Choose a default with `style`, or select one for a command with `--style`:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
gityo --style team --generate
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Custom `styles` extend the built-ins. A custom style with the same name replaces
|
|
193
|
+
the built-in style. A style can be inline text or an object with a `path` to a
|
|
194
|
+
prompt file. File paths are passed to Node's `path.resolve()`, so relative paths
|
|
195
|
+
resolve from the directory where you run `gityo`.
|
|
196
|
+
|
|
197
|
+
Style selection priority is `--style`, then config `style`, then `default`.
|
|
198
|
+
|
|
199
|
+
`instructions` uses the same format. Set it to a string or `{ "path": "..." }`
|
|
200
|
+
to load additional instructions from a file.
|
|
201
|
+
|
|
202
|
+
### Large changes
|
|
203
|
+
|
|
204
|
+
When the diff is small, it is sent to the model as-is. When it is too large for one request, gityo minimizes it first:
|
|
205
|
+
|
|
206
|
+
- regenerates the diff with minimal context lines
|
|
207
|
+
- drops lock files and minified/generated files from the payload
|
|
208
|
+
- caps each file's patch and lists every changed file with its line counts so the model still sees the full picture
|
|
209
|
+
|
|
210
|
+
If the minimized diff is still too large, each remaining part is summarized in parallel and a final commit message is generated from those summaries.
|
|
211
|
+
|
|
212
|
+
Two optional config knobs control this:
|
|
213
|
+
|
|
214
|
+
- `maxDiffTokens` — estimated token budget for the diff sent to the model (default `24000`)
|
|
215
|
+
- `perFileCap` — max diff lines kept per file when minimizing (default `400`)
|
|
216
|
+
|
|
161
217
|
Example instructions file:
|
|
162
218
|
|
|
163
219
|
```md
|
|
@@ -169,5 +225,5 @@ Keep the subject line under 72 characters.
|
|
|
169
225
|
Priority is simple:
|
|
170
226
|
|
|
171
227
|
- `.gityo.md` for repo-specific instructions
|
|
172
|
-
- `.gityo.
|
|
228
|
+
- `.gityo.json` for project config
|
|
173
229
|
- `~/.config/gityo.json` for your defaults
|
package/dist/index.js
CHANGED
|
@@ -1710,7 +1710,7 @@ function useColor() {
|
|
|
1710
1710
|
if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
|
|
1711
1711
|
}
|
|
1712
1712
|
new Command();
|
|
1713
|
-
var version$4 = "1.0.
|
|
1713
|
+
var version$4 = "1.0.12";
|
|
1714
1714
|
Object.freeze({ status: "aborted" });
|
|
1715
1715
|
function $constructor$3(name$16, initializer$6, params) {
|
|
1716
1716
|
function init$1(inst, def) {
|
|
@@ -175933,25 +175933,34 @@ const modelSchema = object$4({
|
|
|
175933
175933
|
apiUrl: url$3().optional(),
|
|
175934
175934
|
options: record$3(string$6(), unknown$3()).optional()
|
|
175935
175935
|
});
|
|
175936
|
+
const instructionSchema = union$4([string$6().min(1), object$4({ path: string$6().min(1) })]);
|
|
175936
175937
|
const configSchema = object$4({
|
|
175937
175938
|
$schema: url$3(),
|
|
175938
175939
|
models: record$3(string$6().min(1), modelSchema),
|
|
175940
|
+
instructions: instructionSchema,
|
|
175941
|
+
style: string$6().min(1),
|
|
175942
|
+
styles: record$3(string$6().min(1), instructionSchema),
|
|
175943
|
+
maxDiffTokens: number$5().int().min(1e3),
|
|
175944
|
+
perFileCap: number$5().int().min(50),
|
|
175939
175945
|
autoAcceptMessage: boolean$5(),
|
|
175940
|
-
|
|
175941
|
-
postCommand: _enum$4(["push", "push-and-pull"]).nullable()
|
|
175942
|
-
autoRunPostCommand: boolean$5()
|
|
175946
|
+
autoRunPostCommand: boolean$5(),
|
|
175947
|
+
postCommand: _enum$4(["push", "push-and-pull"]).nullable()
|
|
175943
175948
|
}).partial();
|
|
175944
175949
|
function resolveConfig(input) {
|
|
175945
175950
|
const parsed = configSchema.parse(input);
|
|
175946
175951
|
return {
|
|
175947
175952
|
models: parsed.models,
|
|
175953
|
+
style: parsed.style,
|
|
175954
|
+
styles: parsed.styles,
|
|
175948
175955
|
instructions: parsed.instructions,
|
|
175956
|
+
maxDiffTokens: parsed.maxDiffTokens,
|
|
175957
|
+
perFileCap: parsed.perFileCap,
|
|
175949
175958
|
autoAcceptCommitMessage: parsed.autoAcceptMessage ?? false,
|
|
175950
|
-
|
|
175951
|
-
|
|
175959
|
+
autoRunPostCommand: parsed.autoRunPostCommand ?? false,
|
|
175960
|
+
postCommand: parsed.postCommand === void 0 ? "push" : parsed.postCommand
|
|
175952
175961
|
};
|
|
175953
175962
|
}
|
|
175954
|
-
const PROJECT_CONFIG_FILE_NAME = ".gityo.
|
|
175963
|
+
const PROJECT_CONFIG_FILE_NAME = ".gityo.json";
|
|
175955
175964
|
const GLOBAL_CONFIG_FILE_PATH = path.join(os.homedir(), ".config", "gityo.json");
|
|
175956
175965
|
async function loadConfig(cwd = process.cwd()) {
|
|
175957
175966
|
const [globalConfig$3, projectConfig, projectInstructions] = await Promise.all([
|
|
@@ -181074,17 +181083,23 @@ async function getChangedFiles(git) {
|
|
|
181074
181083
|
...splitNull(untracked)
|
|
181075
181084
|
])).sort((left, right) => left.localeCompare(right));
|
|
181076
181085
|
}
|
|
181077
|
-
async function getCommitDiff(git) {
|
|
181086
|
+
async function getCommitDiff(git, contextLines = 3) {
|
|
181087
|
+
const unified = [`-U${contextLines}`];
|
|
181078
181088
|
const staged = await git.raw([
|
|
181079
181089
|
"diff",
|
|
181080
181090
|
"--cached",
|
|
181081
|
-
"--no-ext-diff"
|
|
181091
|
+
"--no-ext-diff",
|
|
181092
|
+
...unified
|
|
181082
181093
|
]);
|
|
181083
181094
|
if (staged.trim().length > 0) return {
|
|
181084
181095
|
diff: staged,
|
|
181085
181096
|
hasStaged: true
|
|
181086
181097
|
};
|
|
181087
|
-
const [unstaged, untracked] = await Promise.all([git.raw([
|
|
181098
|
+
const [unstaged, untracked] = await Promise.all([git.raw([
|
|
181099
|
+
"diff",
|
|
181100
|
+
"--no-ext-diff",
|
|
181101
|
+
...unified
|
|
181102
|
+
]), git.raw([
|
|
181088
181103
|
"ls-files",
|
|
181089
181104
|
"--others",
|
|
181090
181105
|
"--exclude-standard",
|
|
@@ -181094,6 +181109,7 @@ async function getCommitDiff(git) {
|
|
|
181094
181109
|
diff: [unstaged, ...await Promise.all(splitNull(untracked).map((file$4) => git.raw([
|
|
181095
181110
|
"diff",
|
|
181096
181111
|
"--no-index",
|
|
181112
|
+
...unified,
|
|
181097
181113
|
"--",
|
|
181098
181114
|
"/dev/null",
|
|
181099
181115
|
file$4
|
|
@@ -181112,20 +181128,177 @@ function createLiveGit(baseDir) {
|
|
|
181112
181128
|
});
|
|
181113
181129
|
return git;
|
|
181114
181130
|
}
|
|
181115
|
-
|
|
181116
|
-
|
|
181131
|
+
const DEFAULT_MAX_DIFF_TOKENS = 24e3;
|
|
181132
|
+
const DEFAULT_PER_FILE_CAP = 400;
|
|
181133
|
+
const CHARS_PER_TOKEN = 4;
|
|
181134
|
+
const MINIMIZED_CONTEXT_LINES = 1;
|
|
181135
|
+
const FILE_SECTION_PREFIX = "diff --git ";
|
|
181136
|
+
const HUNK_HEADER_PREFIX = "@@ ";
|
|
181137
|
+
const NOISE_FILE_PATTERNS = [/\.(lock|min\.(?:js|mjs|cjs|css)|map)$/i, /-lock\.[^/]+$/i];
|
|
181138
|
+
function estimateTokens(text$1) {
|
|
181139
|
+
return Math.ceil(text$1.length / CHARS_PER_TOKEN);
|
|
181140
|
+
}
|
|
181141
|
+
async function minimizeDiff(git, options) {
|
|
181142
|
+
const { diff } = await getCommitDiff(git, MINIMIZED_CONTEXT_LINES);
|
|
181143
|
+
const sections = splitFileSections(diff).map(parseSection);
|
|
181144
|
+
const kept = [];
|
|
181145
|
+
const excludedPaths = /* @__PURE__ */ new Set();
|
|
181146
|
+
for (const section of sections) if (isNoiseFile(section.path)) excludedPaths.add(section.path);
|
|
181147
|
+
else kept.push(section);
|
|
181148
|
+
return {
|
|
181149
|
+
toc: buildToc(sections, excludedPaths, options.allFiles),
|
|
181150
|
+
body: kept.map((section) => capSection(section, options.perFileCap)).join("\n")
|
|
181151
|
+
};
|
|
181152
|
+
}
|
|
181153
|
+
function splitDiffIntoChunks(diff, maxTokens) {
|
|
181154
|
+
return packIntoChunks(splitFileSections(diff).flatMap((section) => estimateTokens(section) <= maxTokens ? [section] : splitOversizedSection(section, maxTokens)), maxTokens);
|
|
181155
|
+
}
|
|
181156
|
+
function isNoiseFile(path$4) {
|
|
181157
|
+
return NOISE_FILE_PATTERNS.some((pattern) => pattern.test(path$4));
|
|
181158
|
+
}
|
|
181159
|
+
function splitFileSections(diff) {
|
|
181160
|
+
return ("\n" + diff.trimStart()).split("\ndiff --git ").slice(1).map((part) => FILE_SECTION_PREFIX + part);
|
|
181161
|
+
}
|
|
181162
|
+
function parseSection(section) {
|
|
181163
|
+
let additions = 0;
|
|
181164
|
+
let deletions = 0;
|
|
181165
|
+
let binary = false;
|
|
181166
|
+
for (const line of section.split("\n").slice(1)) {
|
|
181167
|
+
if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
|
|
181168
|
+
binary = true;
|
|
181169
|
+
continue;
|
|
181170
|
+
}
|
|
181171
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
181172
|
+
if (line.startsWith("+")) additions++;
|
|
181173
|
+
else if (line.startsWith("-")) deletions++;
|
|
181174
|
+
}
|
|
181175
|
+
return {
|
|
181176
|
+
path: extractPath(section.split("\n")[0]),
|
|
181177
|
+
content: section,
|
|
181178
|
+
additions,
|
|
181179
|
+
deletions,
|
|
181180
|
+
binary
|
|
181181
|
+
};
|
|
181182
|
+
}
|
|
181183
|
+
function extractPath(header) {
|
|
181184
|
+
const match = header.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
181185
|
+
return match ? match[2] : "";
|
|
181186
|
+
}
|
|
181187
|
+
function capSection(section, cap) {
|
|
181188
|
+
const lines = section.content.split("\n");
|
|
181189
|
+
const bodyLines = lines.length - 1;
|
|
181190
|
+
if (bodyLines <= cap) return section.content;
|
|
181191
|
+
return [...lines.slice(0, cap + 1), `[... ${bodyLines - cap} more lines truncated]`].join("\n");
|
|
181192
|
+
}
|
|
181193
|
+
function buildToc(sections, excludedPaths, allFiles) {
|
|
181194
|
+
const lines = ["Changed files:"];
|
|
181195
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181196
|
+
for (const section of sections) {
|
|
181197
|
+
seen.add(section.path);
|
|
181198
|
+
if (section.binary) {
|
|
181199
|
+
lines.push(`- ${section.path} (binary)`);
|
|
181200
|
+
continue;
|
|
181201
|
+
}
|
|
181202
|
+
lines.push(`- ${section.path} (+${section.additions} -${section.deletions})`);
|
|
181203
|
+
}
|
|
181204
|
+
for (const path$4 of excludedPaths) {
|
|
181205
|
+
seen.add(path$4);
|
|
181206
|
+
lines.push(`- ${path$4} (excluded generated file)`);
|
|
181207
|
+
}
|
|
181208
|
+
for (const file$4 of allFiles) if (!seen.has(file$4)) lines.push(`- ${file$4}`);
|
|
181209
|
+
return lines.join("\n");
|
|
181210
|
+
}
|
|
181211
|
+
function packIntoChunks(pieces, maxTokens) {
|
|
181212
|
+
const chunks = [];
|
|
181213
|
+
let current = "";
|
|
181214
|
+
for (const piece of pieces) {
|
|
181215
|
+
if (current.length > 0 && estimateTokens(current + piece) > maxTokens) {
|
|
181216
|
+
chunks.push(current);
|
|
181217
|
+
current = "";
|
|
181218
|
+
}
|
|
181219
|
+
current += piece;
|
|
181220
|
+
}
|
|
181221
|
+
if (current.length > 0) chunks.push(current);
|
|
181222
|
+
return chunks;
|
|
181223
|
+
}
|
|
181224
|
+
function splitOversizedSection(section, maxTokens) {
|
|
181225
|
+
const [header, ...lines] = section.split("\n");
|
|
181226
|
+
const hunks = [];
|
|
181227
|
+
let currentHunk = [];
|
|
181228
|
+
for (const line of lines) {
|
|
181229
|
+
if (line.startsWith(HUNK_HEADER_PREFIX) && currentHunk.length > 0) {
|
|
181230
|
+
hunks.push(currentHunk);
|
|
181231
|
+
currentHunk = [];
|
|
181232
|
+
}
|
|
181233
|
+
currentHunk.push(line);
|
|
181234
|
+
}
|
|
181235
|
+
if (currentHunk.length > 0) hunks.push(currentHunk);
|
|
181236
|
+
return packIntoChunks(hunks.flatMap((hunk) => {
|
|
181237
|
+
const piece = [header, ...hunk].join("\n");
|
|
181238
|
+
return estimateTokens(piece) <= maxTokens ? [piece] : hardSplit(piece, maxTokens);
|
|
181239
|
+
}), maxTokens);
|
|
181240
|
+
}
|
|
181241
|
+
function hardSplit(text$1, maxTokens) {
|
|
181242
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
181243
|
+
const chunks = [];
|
|
181244
|
+
let current = "";
|
|
181245
|
+
for (const line of text$1.split("\n")) {
|
|
181246
|
+
if (estimateTokens(line) > maxTokens) {
|
|
181247
|
+
if (current.length > 0) {
|
|
181248
|
+
chunks.push(current);
|
|
181249
|
+
current = "";
|
|
181250
|
+
}
|
|
181251
|
+
for (let index$2 = 0; index$2 < line.length; index$2 += maxChars) chunks.push(line.slice(index$2, index$2 + maxChars));
|
|
181252
|
+
continue;
|
|
181253
|
+
}
|
|
181254
|
+
const candidate = current.length === 0 ? line : `${current}\n${line}`;
|
|
181255
|
+
if (estimateTokens(candidate) > maxTokens) {
|
|
181256
|
+
chunks.push(current);
|
|
181257
|
+
current = line;
|
|
181258
|
+
continue;
|
|
181259
|
+
}
|
|
181260
|
+
current = candidate;
|
|
181261
|
+
}
|
|
181262
|
+
if (current.length > 0) chunks.push(current);
|
|
181263
|
+
return chunks;
|
|
181264
|
+
}
|
|
181265
|
+
const summarizePrompt = `You summarize parts of a large git diff for a commit message generator.
|
|
181266
|
+
Describe WHAT changed, in which files or modules, and any observable intent.
|
|
181267
|
+
Be factual and terse. Do NOT write a commit message. Maximum 120 words.`;
|
|
181268
|
+
async function ask(languageModel, system, messages) {
|
|
181117
181269
|
return (await generateText({
|
|
181118
181270
|
model: languageModel,
|
|
181119
|
-
instructions:
|
|
181120
|
-
messages
|
|
181121
|
-
role: "user",
|
|
181122
|
-
content: `Changes:\n${diff}`
|
|
181123
|
-
}, {
|
|
181124
|
-
role: "user",
|
|
181125
|
-
content: instructions || "Generate a concise git commit message based on the above instructions and diff."
|
|
181126
|
-
}]
|
|
181271
|
+
instructions: system,
|
|
181272
|
+
messages
|
|
181127
181273
|
})).text.trim();
|
|
181128
181274
|
}
|
|
181275
|
+
async function generateCommitMessage(languageModel, style, instructions, diff) {
|
|
181276
|
+
return ask(languageModel, style, [{
|
|
181277
|
+
role: "user",
|
|
181278
|
+
content: `Changes:\n${diff}`
|
|
181279
|
+
}, {
|
|
181280
|
+
role: "user",
|
|
181281
|
+
content: instructions || "Generate a concise git commit message based on the above instructions and diff."
|
|
181282
|
+
}]);
|
|
181283
|
+
}
|
|
181284
|
+
async function summarizeChanges(languageModel, toc, chunk$1) {
|
|
181285
|
+
return ask(languageModel, summarizePrompt, [{
|
|
181286
|
+
role: "user",
|
|
181287
|
+
content: `All changed files:\n${toc}\n\nChanges (part of a larger diff):\n${chunk$1}`
|
|
181288
|
+
}, {
|
|
181289
|
+
role: "user",
|
|
181290
|
+
content: "Summarize these changes."
|
|
181291
|
+
}]);
|
|
181292
|
+
}
|
|
181293
|
+
async function generateCommitMessageFromSummaries(llm, style, instructions, toc, summaries) {
|
|
181294
|
+
return ask(llm, style, [{
|
|
181295
|
+
role: "user",
|
|
181296
|
+
content: `All changed files:\n${toc}\n\nSummaries of all changes:\n${summaries.map((summary, index$2) => `Part ${index$2 + 1}:\n${summary}`).join("\n\n")}`
|
|
181297
|
+
}, {
|
|
181298
|
+
role: "user",
|
|
181299
|
+
content: instructions || "Generate a concise git commit message covering ALL parts above."
|
|
181300
|
+
}]);
|
|
181301
|
+
}
|
|
181129
181302
|
function resolveLanguageModel(modelConfig) {
|
|
181130
181303
|
const envVarNames = Array.isArray(modelConfig.apiKeyEnv) ? modelConfig.apiKeyEnv : [modelConfig.apiKeyEnv];
|
|
181131
181304
|
let apiKey = null;
|
|
@@ -181146,6 +181319,28 @@ function resolveLanguageModel(modelConfig) {
|
|
|
181146
181319
|
...modelConfig.options
|
|
181147
181320
|
}).languageModel(modelConfig.model);
|
|
181148
181321
|
}
|
|
181322
|
+
const BUILTIN_STYLES = {
|
|
181323
|
+
default: "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n\n- First line: <type>(<scope>): <subject> (max 50 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- Optional body: blank line + concise explanation (wrapped at 72 chars)\n\nSTRICT RULES:\n\n- MUST use conventional commits format: type(scope): subject\n- Subject line MUST be under 50 characters\n- Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n- Subject MUST be lowercase\n- Subject MUST NOT end with a period\n- MUST provide clear context about WHAT changed\n- MUST be specific about affected functions, components, or modules\n- MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n- Do NOT include a body for a small or focused change\n- Include a body only when the diff contains several substantial related changes and explaining them adds useful context\n- When a body is warranted, explain WHY when the rationale or impact is not obvious; do not force or invent a reason\n- If including a body, MUST have exactly one blank line between subject and body\n- Body lines MUST wrap at 72 characters\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\nfeat(llm): add streaming responses\n\nAnalyze the diff and generate a message following ALL rules above.\n",
|
|
181324
|
+
concise: "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- First line: <type>(<scope>): <subject> (max 50 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- Exceptional body: blank line + minimal explanation (wrapped at 72 chars)\n\nSTRICT RULES:\n- MUST use conventional commits format: type(scope): subject\n- Subject line MUST be under 50 characters\n- Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n- Subject MUST be lowercase\n- Subject MUST NOT end with a period\n- MUST provide clear context about the most important change\n- MUST be specific about affected functions, components, or modules\n- MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n- MUST prefer a subject line with no body\n- Include a body only when absolutely necessary to communicate critical context that cannot be expressed in the subject\n- If including a body, keep it as short as possible and explain WHY only when that rationale or impact is essential and supported by the diff\n- If including a body, MUST have exactly one blank line between subject and body\n- Body lines MUST wrap at 72 characters\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\nfix(config): preserve provider settings\n\nAnalyze the diff and generate a message following ALL rules above.\n",
|
|
181325
|
+
explanatory: "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- First line: <type>(<scope>): <subject> (max 72 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- Encouraged body: blank line + concise explanation (wrapped at 72 chars)\n\nSTRICT RULES:\n- MUST use conventional commits format: type(scope): subject\n- Subject line MUST be under 72 characters\n- Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n- Subject MUST be lowercase\n- Subject MUST NOT end with a period\n- MUST provide clear context about WHAT changed\n- MUST be specific about affected functions, components, or modules\n- MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n- Include a body whenever it adds meaningful context beyond the subject\n- Use the body to explain WHY the change was made or its impact when supported by the diff; do not force or invent a reason\n- Omit the body only when it would merely repeat the subject\n- If including a body, MUST have exactly one blank line between subject and body\n- Body lines MUST wrap at 72 characters\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\nfix(config): preserve custom provider settings\n\nReload stored provider options before applying command-line overrides to\nprevent unrelated settings from being discarded.\n\nAnalyze the diff and generate a message following ALL rules above.\n",
|
|
181326
|
+
plain: "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- Only line: <subject> (max 72 characters)\n- Subject: imperative mood, lowercase, no punctuation\n- No prefix, scope, emoji, or body\n\nSTRICT RULES:\n- MUST output exactly one plain subject line\n- Complete line MUST be under 72 characters\n- Subject MUST start with an imperative verb (e.g., \"add\", \"fix\", \"update\")\n- Subject MUST be lowercase\n- Subject MUST NOT end with punctuation\n- MUST provide clear context about the most important change\n- MUST be specific about affected functions, components, or modules\n- MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n- MUST NOT use conventional commit types, scopes, prefixes, or emoji\n- MUST NOT include a body\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\npreserve custom provider settings\n\nAnalyze the diff and generate a message following ALL rules above.\n",
|
|
181327
|
+
gitmoji: "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- Only line: <gitmoji> <type>(<scope>): <subject> (max 72 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- No body\n\nSTRICT RULES:\n- MUST use gitmoji conventional commits format: <gitmoji> type(scope): subject\n- MUST choose exactly one gitmoji that best represents the change\n- Use relevant gitmoji such as ✨ for features, 🐛 for fixes, 📝 for documentation, ♻️ for refactoring, ✅ for tests, or 🔧 for configuration\n- Complete line MUST be under 72 characters\n- Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n- Subject MUST be lowercase\n- Subject MUST NOT end with a period\n- MUST provide clear context about the most important change\n- MUST be specific about affected functions, components, or modules\n- MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n- MUST NOT include a body\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\n✨ feat(cli): add interactive style selection\n\nAnalyze the diff and generate a message following ALL rules above.\n"
|
|
181328
|
+
};
|
|
181329
|
+
function getStyleKeys(styles$2) {
|
|
181330
|
+
return Object.keys({
|
|
181331
|
+
...BUILTIN_STYLES,
|
|
181332
|
+
...styles$2
|
|
181333
|
+
});
|
|
181334
|
+
}
|
|
181335
|
+
async function resolveStyle(styleKey, styles$2) {
|
|
181336
|
+
const style = styles$2?.[styleKey] ?? BUILTIN_STYLES[styleKey];
|
|
181337
|
+
if (!style) return;
|
|
181338
|
+
return resolveInstructionContent(style);
|
|
181339
|
+
}
|
|
181340
|
+
async function resolveInstructionContent(instruction) {
|
|
181341
|
+
if (typeof instruction === "string") return instruction;
|
|
181342
|
+
return readFile(path.resolve(instruction.path), "utf8");
|
|
181343
|
+
}
|
|
181149
181344
|
const isUpKey = (key$1, keybindings = []) => key$1.name === "up" || keybindings.includes("vim") && key$1.name === "k" || keybindings.includes("emacs") && key$1.ctrl && key$1.name === "p";
|
|
181150
181345
|
const isDownKey = (key$1, keybindings = []) => key$1.name === "down" || keybindings.includes("vim") && key$1.name === "j" || keybindings.includes("emacs") && key$1.ctrl && key$1.name === "n";
|
|
181151
181346
|
const isSpaceKey = (key$1) => key$1.name === "space";
|
|
@@ -197948,7 +198143,11 @@ async function runWithLoading(label, task) {
|
|
|
197948
198143
|
let index$2 = 0;
|
|
197949
198144
|
const interval = setInterval(() => {
|
|
197950
198145
|
readline.cursorTo(process.stdout, 0);
|
|
197951
|
-
process.stdout.write(
|
|
198146
|
+
process.stdout.write([
|
|
198147
|
+
source_default.cyan(frames[index$2]) + " ",
|
|
198148
|
+
source_default.cyan(label),
|
|
198149
|
+
source_default.reset.dim("...")
|
|
198150
|
+
].join(""));
|
|
197952
198151
|
index$2 = (index$2 + 1) % frames.length;
|
|
197953
198152
|
}, 80);
|
|
197954
198153
|
try {
|
|
@@ -197959,6 +198158,10 @@ async function runWithLoading(label, task) {
|
|
|
197959
198158
|
readline.clearLine(process.stdout, 0);
|
|
197960
198159
|
}
|
|
197961
198160
|
}
|
|
198161
|
+
const MAP_CONCURRENCY = 3;
|
|
198162
|
+
const PROMPT_RESERVE_TOKENS = 800;
|
|
198163
|
+
const MIN_CHUNK_BUDGET_TOKENS = 1e3;
|
|
198164
|
+
const MAX_TOC_LINES = 500;
|
|
197962
198165
|
async function mainController(options = {}) {
|
|
197963
198166
|
const { git, liveGit } = await getGit();
|
|
197964
198167
|
const config$4 = await loadConfig((await git.revparse(["--show-toplevel"])).trim());
|
|
@@ -197970,10 +198173,17 @@ async function mainController(options = {}) {
|
|
|
197970
198173
|
throw new Error(hint);
|
|
197971
198174
|
}
|
|
197972
198175
|
const languageModel = resolveLanguageModel(modelConfig);
|
|
198176
|
+
const styleKey = options.style ?? config$4.style ?? "default";
|
|
198177
|
+
const style = await resolveStyle(styleKey, config$4.styles);
|
|
198178
|
+
if (!style) {
|
|
198179
|
+
const availableStyles = getStyleKeys(config$4.styles).join(", ");
|
|
198180
|
+
throw new Error(`Style '${styleKey}' is not configured. Available styles: ${availableStyles}.`);
|
|
198181
|
+
}
|
|
198182
|
+
const instructions = config$4.instructions ? await resolveInstructionContent(config$4.instructions) : null;
|
|
197973
198183
|
const forceLLMGenerate = options.generate || options.yolo;
|
|
197974
198184
|
const forceExecPostCommand = options.post || options.yolo;
|
|
197975
|
-
let finalCommitMessage = options.
|
|
197976
|
-
if (typeof options.
|
|
198185
|
+
let finalCommitMessage = options.input?.trim() ?? "";
|
|
198186
|
+
if (typeof options.input === "string" && finalCommitMessage.length === 0) throw new Error("Provided commit message input cannot be empty.");
|
|
197977
198187
|
const files = await getChangedFiles(git);
|
|
197978
198188
|
if (files.length === 0) return console.log("No changed files found.");
|
|
197979
198189
|
const { diff, hasStaged } = await getCommitDiff(git);
|
|
@@ -197992,7 +198202,16 @@ async function mainController(options = {}) {
|
|
|
197992
198202
|
}
|
|
197993
198203
|
if (finalCommitMessage.length === 0) while (true) {
|
|
197994
198204
|
if (forceLLMGenerate) console.log(source_default.yellow("• Using LLM to generate message"));
|
|
197995
|
-
finalCommitMessage = (await runWithLoading("Generating commit message", () =>
|
|
198205
|
+
finalCommitMessage = (await runWithLoading("Generating commit message", () => generateMessage({
|
|
198206
|
+
git,
|
|
198207
|
+
languageModel,
|
|
198208
|
+
style,
|
|
198209
|
+
instructions,
|
|
198210
|
+
diff,
|
|
198211
|
+
files,
|
|
198212
|
+
perFileCap: config$4.perFileCap ?? DEFAULT_PER_FILE_CAP,
|
|
198213
|
+
maxDiffTokens: config$4.maxDiffTokens ?? DEFAULT_MAX_DIFF_TOKENS
|
|
198214
|
+
}))).trim();
|
|
197996
198215
|
if (finalCommitMessage.length === 0) throw new Error("The selected model returned an empty commit message.");
|
|
197997
198216
|
console.log(source_default.cyan.dim(finalCommitMessage));
|
|
197998
198217
|
console.log("");
|
|
@@ -198012,6 +198231,32 @@ async function mainController(options = {}) {
|
|
|
198012
198231
|
await liveGit.push();
|
|
198013
198232
|
if (config$4.postCommand === "push-and-pull") await liveGit.pull();
|
|
198014
198233
|
}
|
|
198234
|
+
async function generateMessage(options) {
|
|
198235
|
+
const { git, languageModel, style, instructions, diff } = options;
|
|
198236
|
+
if (estimateTokens(diff) <= options.maxDiffTokens) return generateCommitMessage(languageModel, style, instructions, diff);
|
|
198237
|
+
const { toc, body } = await minimizeDiff(git, {
|
|
198238
|
+
perFileCap: options.perFileCap,
|
|
198239
|
+
allFiles: options.files
|
|
198240
|
+
});
|
|
198241
|
+
if (estimateTokens(toc) + estimateTokens(body) <= options.maxDiffTokens) return generateCommitMessage(languageModel, style, instructions, `${toc}\n\n${body}`);
|
|
198242
|
+
let effectiveToc = toc;
|
|
198243
|
+
let chunkBudget = options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS;
|
|
198244
|
+
if (chunkBudget < MIN_CHUNK_BUDGET_TOKENS) {
|
|
198245
|
+
effectiveToc = toc.split("\n").slice(0, MAX_TOC_LINES).join("\n");
|
|
198246
|
+
chunkBudget = Math.max(options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS, MIN_CHUNK_BUDGET_TOKENS);
|
|
198247
|
+
}
|
|
198248
|
+
const chunks = splitDiffIntoChunks(body, chunkBudget);
|
|
198249
|
+
const summaries = new Array(chunks.length);
|
|
198250
|
+
let nextChunk = 0;
|
|
198251
|
+
async function worker() {
|
|
198252
|
+
while (nextChunk < chunks.length) {
|
|
198253
|
+
const index$2 = nextChunk++;
|
|
198254
|
+
summaries[index$2] = await summarizeChanges(languageModel, effectiveToc, chunks[index$2]);
|
|
198255
|
+
}
|
|
198256
|
+
}
|
|
198257
|
+
await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, chunks.length) }, worker));
|
|
198258
|
+
return generateCommitMessageFromSummaries(languageModel, style, instructions, effectiveToc, summaries);
|
|
198259
|
+
}
|
|
198015
198260
|
var AppUserCanceledError = class extends Error {};
|
|
198016
198261
|
function handleError(fn$1) {
|
|
198017
198262
|
Promise.resolve().then(fn$1).catch((err) => {
|
|
@@ -198022,9 +198267,9 @@ function handleError(fn$1) {
|
|
|
198022
198267
|
}
|
|
198023
198268
|
});
|
|
198024
198269
|
}
|
|
198025
|
-
const app = new Command().name("gityo").
|
|
198026
|
-
if (options.generate && options.
|
|
198027
|
-
console.error("Cannot use --generate and --
|
|
198270
|
+
const app = new Command().name("gityo").description("Stage changes, generate or enter a commit message, create a commit, and run a post-commit git command.").option("-i, --input <input>", "Use the provided input as the commit message.").option("-s, --style <style>", "Commit message style key to use (defaults to \"default\").").option("-m, --model <model>", "Model key from config to use (defaults to \"default\").").option("-g, --generate", "Generate a commit message and commit without asking.").option("-p, --post", "Run the post-commit git command without asking.").option("-y, --yolo", "Skip all questions, and generate message, commit, run post command. [Will fail if no model available]").version(`v${version$4}`, "-v, --version", "Show the current version.").action((options) => {
|
|
198271
|
+
if (options.generate && options.input) {
|
|
198272
|
+
console.error("Cannot use --generate and --input together.");
|
|
198028
198273
|
process.exit(1);
|
|
198029
198274
|
}
|
|
198030
198275
|
handleError(() => mainController(options));
|
package/package.json
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gityo",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/NazmusSayad/gityo.git"
|
|
7
|
+
},
|
|
4
8
|
"sideEffects": false,
|
|
5
9
|
"type": "module",
|
|
6
10
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"typecheck:watch": "tsc --noEmit --watch",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
10
15
|
"build": "tsdown --config ./tsdown.config.ts",
|
|
11
|
-
"dev": "
|
|
12
|
-
"
|
|
16
|
+
"dev": "tsdown --config ./tsdown.config.ts --watch",
|
|
17
|
+
"start": "node --watch ./dist/index.js",
|
|
13
18
|
"json": "tsx ./src/json.ts"
|
|
14
19
|
},
|
|
15
20
|
"bin": {
|
|
16
|
-
"gityo": "
|
|
21
|
+
"gityo": "dist/index.js"
|
|
17
22
|
},
|
|
18
23
|
"devDependencies": {
|
|
19
24
|
"@ai-sdk/amazon-bedrock": "^5.0.40",
|