code-agent-auto-commit 1.0.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/.code-agent-auto-commit.example.json +53 -0
- package/CODE_OF_CONDUCT.md +15 -0
- package/CONTRIBUTING.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SECURITY.md +17 -0
- package/dist/adapters/claude.d.ts +20 -0
- package/dist/adapters/claude.js +134 -0
- package/dist/adapters/codex.d.ts +13 -0
- package/dist/adapters/codex.js +71 -0
- package/dist/adapters/opencode.d.ts +13 -0
- package/dist/adapters/opencode.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +281 -0
- package/dist/core/ai.d.ts +2 -0
- package/dist/core/ai.js +205 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.js +181 -0
- package/dist/core/exec.d.ts +7 -0
- package/dist/core/exec.js +24 -0
- package/dist/core/filter.d.ts +2 -0
- package/dist/core/filter.js +45 -0
- package/dist/core/fs.d.ts +7 -0
- package/dist/core/fs.js +46 -0
- package/dist/core/git.d.ts +10 -0
- package/dist/core/git.js +108 -0
- package/dist/core/run.d.ts +2 -0
- package/dist/core/run.js +148 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/test/config.test.d.ts +1 -0
- package/dist/test/config.test.js +23 -0
- package/dist/test/filter.test.d.ts +1 -0
- package/dist/test/filter.test.js +14 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +2 -0
- package/docs/CONFIG.md +72 -0
- package/docs/zh-CN.md +81 -0
- package/package.json +57 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const claude_1 = require("./adapters/claude");
|
|
10
|
+
const codex_1 = require("./adapters/codex");
|
|
11
|
+
const opencode_1 = require("./adapters/opencode");
|
|
12
|
+
const config_1 = require("./core/config");
|
|
13
|
+
const fs_1 = require("./core/fs");
|
|
14
|
+
const run_1 = require("./core/run");
|
|
15
|
+
function parseOptions(args) {
|
|
16
|
+
const flags = {};
|
|
17
|
+
const positionals = [];
|
|
18
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (!arg.startsWith("--")) {
|
|
21
|
+
positionals.push(arg);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const trimmed = arg.slice(2);
|
|
25
|
+
const eqIndex = trimmed.indexOf("=");
|
|
26
|
+
if (eqIndex >= 0) {
|
|
27
|
+
const key = trimmed.slice(0, eqIndex);
|
|
28
|
+
const value = trimmed.slice(eqIndex + 1);
|
|
29
|
+
flags[key] = value;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const next = args[i + 1];
|
|
33
|
+
if (!next || next.startsWith("--")) {
|
|
34
|
+
flags[trimmed] = true;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
flags[trimmed] = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { flags, positionals };
|
|
42
|
+
}
|
|
43
|
+
function getStringFlag(flags, key) {
|
|
44
|
+
const value = flags[key];
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function getBooleanFlag(flags, key) {
|
|
51
|
+
return flags[key] === true;
|
|
52
|
+
}
|
|
53
|
+
function parseScope(value) {
|
|
54
|
+
if (!value || value === "project") {
|
|
55
|
+
return "project";
|
|
56
|
+
}
|
|
57
|
+
if (value === "global") {
|
|
58
|
+
return "global";
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Invalid scope: ${value}`);
|
|
61
|
+
}
|
|
62
|
+
function parseTools(value) {
|
|
63
|
+
if (!value || value === "all") {
|
|
64
|
+
return ["opencode", "codex", "claude"];
|
|
65
|
+
}
|
|
66
|
+
const allowed = new Set(["opencode", "codex", "claude"]);
|
|
67
|
+
const tools = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
68
|
+
for (const tool of tools) {
|
|
69
|
+
if (!allowed.has(tool)) {
|
|
70
|
+
throw new Error(`Invalid tool: ${tool}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return tools;
|
|
74
|
+
}
|
|
75
|
+
async function readStdinText() {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of process.stdin) {
|
|
78
|
+
chunks.push(String(chunk));
|
|
79
|
+
}
|
|
80
|
+
return chunks.join("");
|
|
81
|
+
}
|
|
82
|
+
function printHelp() {
|
|
83
|
+
console.log(`cac (code-agent-auto-commit)
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
cac init [--worktree <path>] [--config <path>]
|
|
87
|
+
cac install [--tool all|opencode|codex|claude] [--scope project|global] [--worktree <path>] [--config <path>]
|
|
88
|
+
cac uninstall [--tool all|opencode|codex|claude] [--scope project|global] [--worktree <path>]
|
|
89
|
+
cac status [--scope project|global] [--worktree <path>] [--config <path>]
|
|
90
|
+
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
91
|
+
cac set-worktree <path> [--config <path>]
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
async function commandInit(flags) {
|
|
95
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
96
|
+
const explicit = getStringFlag(flags, "config");
|
|
97
|
+
const configPath = explicit ? node_path_1.default.resolve(explicit) : (0, fs_1.getProjectConfigPath)(worktree);
|
|
98
|
+
(0, config_1.initConfigFile)(configPath, worktree);
|
|
99
|
+
console.log(`Initialized config: ${configPath}`);
|
|
100
|
+
}
|
|
101
|
+
async function commandInstall(flags) {
|
|
102
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
103
|
+
const scope = parseScope(getStringFlag(flags, "scope"));
|
|
104
|
+
const runnerCommand = getStringFlag(flags, "runner") ?? "cac";
|
|
105
|
+
const explicitConfig = getStringFlag(flags, "config");
|
|
106
|
+
const configPath = explicitConfig
|
|
107
|
+
? node_path_1.default.resolve(explicitConfig)
|
|
108
|
+
: node_fs_1.default.existsSync((0, fs_1.getProjectConfigPath)(worktree))
|
|
109
|
+
? (0, fs_1.getProjectConfigPath)(worktree)
|
|
110
|
+
: (0, config_1.resolveConfigPath)({ worktree });
|
|
111
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
112
|
+
(0, config_1.initConfigFile)(configPath, worktree);
|
|
113
|
+
}
|
|
114
|
+
const tools = parseTools(getStringFlag(flags, "tool"));
|
|
115
|
+
for (const tool of tools) {
|
|
116
|
+
if (tool === "opencode") {
|
|
117
|
+
const pluginPath = (0, opencode_1.installOpenCodeAdapter)({
|
|
118
|
+
scope,
|
|
119
|
+
worktree,
|
|
120
|
+
configPath,
|
|
121
|
+
runnerCommand,
|
|
122
|
+
});
|
|
123
|
+
console.log(`OpenCode installed: ${pluginPath}`);
|
|
124
|
+
}
|
|
125
|
+
if (tool === "codex") {
|
|
126
|
+
const codexPath = (0, codex_1.installCodexAdapter)({
|
|
127
|
+
scope,
|
|
128
|
+
worktree,
|
|
129
|
+
configPath,
|
|
130
|
+
runnerCommand,
|
|
131
|
+
});
|
|
132
|
+
console.log(`Codex installed: ${codexPath}`);
|
|
133
|
+
}
|
|
134
|
+
if (tool === "claude") {
|
|
135
|
+
const claude = (0, claude_1.installClaudeAdapter)({
|
|
136
|
+
scope,
|
|
137
|
+
worktree,
|
|
138
|
+
configPath,
|
|
139
|
+
runnerCommand,
|
|
140
|
+
});
|
|
141
|
+
console.log(`Claude hook installed: ${claude.settingsPath}`);
|
|
142
|
+
console.log(`Claude script installed: ${claude.scriptPath}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function commandUninstall(flags) {
|
|
147
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
148
|
+
const scope = parseScope(getStringFlag(flags, "scope"));
|
|
149
|
+
const tools = parseTools(getStringFlag(flags, "tool"));
|
|
150
|
+
for (const tool of tools) {
|
|
151
|
+
if (tool === "opencode") {
|
|
152
|
+
console.log(`OpenCode removed: ${(0, opencode_1.uninstallOpenCodeAdapter)(scope, worktree)}`);
|
|
153
|
+
}
|
|
154
|
+
if (tool === "codex") {
|
|
155
|
+
console.log(`Codex removed: ${(0, codex_1.uninstallCodexAdapter)(scope, worktree)}`);
|
|
156
|
+
}
|
|
157
|
+
if (tool === "claude") {
|
|
158
|
+
const result = (0, claude_1.uninstallClaudeAdapter)(scope, worktree);
|
|
159
|
+
console.log(`Claude hook updated: ${result.settingsPath}`);
|
|
160
|
+
console.log(`Claude script removed: ${result.scriptPath}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function commandStatus(flags) {
|
|
165
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
166
|
+
const scope = parseScope(getStringFlag(flags, "scope"));
|
|
167
|
+
const explicitConfig = getStringFlag(flags, "config");
|
|
168
|
+
const loaded = (0, config_1.loadConfig)({ explicitPath: explicitConfig, worktree });
|
|
169
|
+
console.log(`Config path: ${loaded.path}`);
|
|
170
|
+
console.log(`Worktree: ${loaded.config.worktree}`);
|
|
171
|
+
console.log(`Commit mode: ${loaded.config.commit.mode}`);
|
|
172
|
+
console.log(`AI message: ${loaded.config.ai.enabled ? "enabled" : "disabled"}`);
|
|
173
|
+
console.log(`Auto push: ${loaded.config.push.enabled ? "enabled" : "disabled"}`);
|
|
174
|
+
const opencode = (0, opencode_1.opencodeAdapterStatus)(scope, worktree);
|
|
175
|
+
console.log(`OpenCode adapter: ${opencode.installed ? "installed" : "missing"} (${opencode.path})`);
|
|
176
|
+
const codex = (0, codex_1.codexAdapterStatus)(scope, worktree);
|
|
177
|
+
console.log(`Codex adapter: ${codex.installed ? "installed" : "missing"} (${codex.path})`);
|
|
178
|
+
const claude = (0, claude_1.claudeAdapterStatus)(scope, worktree);
|
|
179
|
+
console.log(`Claude adapter: ${claude.installed ? "installed" : "missing"} (${claude.settingsPath})`);
|
|
180
|
+
}
|
|
181
|
+
async function commandSetWorktree(flags, positionals) {
|
|
182
|
+
const nextWorktree = positionals[0];
|
|
183
|
+
if (!nextWorktree) {
|
|
184
|
+
throw new Error("Missing worktree path");
|
|
185
|
+
}
|
|
186
|
+
const configPath = (0, config_1.resolveConfigPath)({
|
|
187
|
+
explicitPath: getStringFlag(flags, "config"),
|
|
188
|
+
worktree: process.cwd(),
|
|
189
|
+
});
|
|
190
|
+
const updated = (0, config_1.updateConfigWorktree)(configPath, nextWorktree);
|
|
191
|
+
console.log(`Updated config: ${configPath}`);
|
|
192
|
+
console.log(`New worktree: ${updated.worktree}`);
|
|
193
|
+
}
|
|
194
|
+
async function commandRun(flags, positionals) {
|
|
195
|
+
const tool = (getStringFlag(flags, "tool") ?? "manual");
|
|
196
|
+
const worktree = getStringFlag(flags, "worktree");
|
|
197
|
+
const configPath = getStringFlag(flags, "config");
|
|
198
|
+
let event;
|
|
199
|
+
const eventJson = getStringFlag(flags, "event-json");
|
|
200
|
+
if (eventJson) {
|
|
201
|
+
event = JSON.parse(eventJson);
|
|
202
|
+
}
|
|
203
|
+
if (getBooleanFlag(flags, "event-stdin")) {
|
|
204
|
+
const stdinText = (await readStdinText()).trim();
|
|
205
|
+
if (stdinText) {
|
|
206
|
+
event = JSON.parse(stdinText);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!event && positionals.length > 0 && positionals[positionals.length - 1].startsWith("{")) {
|
|
210
|
+
try {
|
|
211
|
+
event = JSON.parse(positionals[positionals.length - 1]);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
event = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (tool === "codex" && event && typeof event === "object") {
|
|
218
|
+
const eventType = event.type;
|
|
219
|
+
if (eventType && eventType !== "agent-turn-complete") {
|
|
220
|
+
console.log(`Skipped: codex event ${eventType}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const result = await (0, run_1.runAutoCommit)({
|
|
225
|
+
tool,
|
|
226
|
+
worktree,
|
|
227
|
+
event,
|
|
228
|
+
sessionID: getStringFlag(flags, "session-id"),
|
|
229
|
+
}, {
|
|
230
|
+
explicitPath: configPath,
|
|
231
|
+
worktree,
|
|
232
|
+
});
|
|
233
|
+
if (result.skipped) {
|
|
234
|
+
console.log(`Skipped: ${result.reason ?? "unknown"}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log(`Committed: ${result.committed.length}`);
|
|
238
|
+
for (const item of result.committed) {
|
|
239
|
+
console.log(`- ${item.hash.slice(0, 12)} ${item.message}`);
|
|
240
|
+
}
|
|
241
|
+
console.log(`Pushed: ${result.pushed ? "yes" : "no"}`);
|
|
242
|
+
}
|
|
243
|
+
async function main() {
|
|
244
|
+
const argv = process.argv.slice(2);
|
|
245
|
+
const command = argv[0];
|
|
246
|
+
const parsed = parseOptions(argv.slice(1));
|
|
247
|
+
if (!command || command === "help" || command === "--help") {
|
|
248
|
+
printHelp();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (command === "init") {
|
|
252
|
+
await commandInit(parsed.flags);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (command === "install") {
|
|
256
|
+
await commandInstall(parsed.flags);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (command === "uninstall") {
|
|
260
|
+
await commandUninstall(parsed.flags);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (command === "status") {
|
|
264
|
+
await commandStatus(parsed.flags);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (command === "set-worktree") {
|
|
268
|
+
await commandSetWorktree(parsed.flags, parsed.positionals);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (command === "run") {
|
|
272
|
+
await commandRun(parsed.flags, parsed.positionals);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`Unknown command: ${command}`);
|
|
276
|
+
}
|
|
277
|
+
main().catch((error) => {
|
|
278
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
279
|
+
console.error(`Error: ${message}`);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
});
|
package/dist/core/ai.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateCommitMessage = generateCommitMessage;
|
|
4
|
+
const DEFAULT_COMMIT_TYPE = "refector";
|
|
5
|
+
function normalizeCommitType(raw) {
|
|
6
|
+
const value = raw.trim().toLowerCase();
|
|
7
|
+
if (value === "feat" || value === "feature") {
|
|
8
|
+
return "feat";
|
|
9
|
+
}
|
|
10
|
+
if (value === "fix" || value === "bugfix" || value === "hotfix") {
|
|
11
|
+
return "fix";
|
|
12
|
+
}
|
|
13
|
+
if (value === "refector"
|
|
14
|
+
|| value === "refactor"
|
|
15
|
+
|| value === "refactoring"
|
|
16
|
+
|| value === "chore"
|
|
17
|
+
|| value === "docs"
|
|
18
|
+
|| value === "style"
|
|
19
|
+
|| value === "test"
|
|
20
|
+
|| value === "perf"
|
|
21
|
+
|| value === "build"
|
|
22
|
+
|| value === "ci"
|
|
23
|
+
|| value === "revert") {
|
|
24
|
+
return "refector";
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function formatTypedMessage(raw, maxLength) {
|
|
29
|
+
const conventional = raw.match(/^([a-zA-Z-]+)(?:\([^)]*\))?\s*:\s*(.+)$/);
|
|
30
|
+
const shorthand = raw.match(/^(feat|feature|fix|bugfix|hotfix|refactor|refector)\b[\s:-]+(.+)$/i);
|
|
31
|
+
const detectedType = normalizeCommitType(conventional?.[1] ?? shorthand?.[1] ?? "");
|
|
32
|
+
const type = detectedType ?? DEFAULT_COMMIT_TYPE;
|
|
33
|
+
const subjectCandidate = (conventional?.[2] ?? shorthand?.[2] ?? raw)
|
|
34
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
35
|
+
.replace(/^[-:]+/, "")
|
|
36
|
+
.trim();
|
|
37
|
+
if (subjectCandidate.length === 0) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
const prefix = `${type}: `;
|
|
41
|
+
const full = `${prefix}${subjectCandidate}`;
|
|
42
|
+
if (full.length <= maxLength) {
|
|
43
|
+
return full;
|
|
44
|
+
}
|
|
45
|
+
const available = maxLength - prefix.length;
|
|
46
|
+
if (available <= 1) {
|
|
47
|
+
return prefix.trimEnd().slice(0, maxLength);
|
|
48
|
+
}
|
|
49
|
+
return `${prefix}${subjectCandidate.slice(0, available - 1).trimEnd()}…`;
|
|
50
|
+
}
|
|
51
|
+
function normalizeMessage(raw, maxLength) {
|
|
52
|
+
const withoutThinking = raw
|
|
53
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "\n")
|
|
54
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "\n")
|
|
55
|
+
.replace(/<\/?(think|thinking)>/gi, "\n");
|
|
56
|
+
const cleaned = withoutThinking
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((line) => line.replace(/^['"`]+|['"`]+$/g, "").trim())
|
|
59
|
+
.find((line) => line.length > 0 && line !== "```")
|
|
60
|
+
?? "";
|
|
61
|
+
if (cleaned.length === 0) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
return formatTypedMessage(cleaned, maxLength);
|
|
65
|
+
}
|
|
66
|
+
function getApiKey(config) {
|
|
67
|
+
if (config.apiKey && config.apiKey.trim().length > 0) {
|
|
68
|
+
return config.apiKey.trim();
|
|
69
|
+
}
|
|
70
|
+
if (!config.apiKeyEnv) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const fromEnv = process.env[config.apiKeyEnv];
|
|
74
|
+
if (!fromEnv) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return fromEnv.trim();
|
|
78
|
+
}
|
|
79
|
+
function splitModelRef(modelRef, defaultProvider) {
|
|
80
|
+
const trimmed = modelRef.trim();
|
|
81
|
+
const slashIndex = trimmed.indexOf("/");
|
|
82
|
+
if (slashIndex === -1) {
|
|
83
|
+
return {
|
|
84
|
+
provider: defaultProvider,
|
|
85
|
+
model: trimmed,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
provider: trimmed.slice(0, slashIndex).trim(),
|
|
90
|
+
model: trimmed.slice(slashIndex + 1).trim(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildUserPrompt(summary, maxLength) {
|
|
94
|
+
return [
|
|
95
|
+
`Generate a short commit message in format "<type>: <subject>" (<= ${maxLength} chars).`,
|
|
96
|
+
"Allowed types: feat, fix, refector.",
|
|
97
|
+
"Changed files:",
|
|
98
|
+
summary.nameStatus || "(none)",
|
|
99
|
+
"Diff stat:",
|
|
100
|
+
summary.diffStat || "(none)",
|
|
101
|
+
"Patch excerpt:",
|
|
102
|
+
summary.patch || "(none)",
|
|
103
|
+
].join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
async function generateOpenAiStyleMessage(provider, model, summary, maxLength, signal) {
|
|
106
|
+
const apiKey = getApiKey(provider);
|
|
107
|
+
const headers = {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
...(provider.headers ?? {}),
|
|
110
|
+
};
|
|
111
|
+
if (apiKey) {
|
|
112
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
113
|
+
}
|
|
114
|
+
const response = await fetch(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
model,
|
|
119
|
+
temperature: 0.2,
|
|
120
|
+
messages: [
|
|
121
|
+
{
|
|
122
|
+
role: "system",
|
|
123
|
+
content: "You generate exactly one commit message line in format '<type>: <subject>'. Type must be one of feat, fix, refector. No quotes. No code block.",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
role: "user",
|
|
127
|
+
content: buildUserPrompt(summary, maxLength),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const payload = (await response.json());
|
|
137
|
+
return payload.choices?.[0]?.message?.content;
|
|
138
|
+
}
|
|
139
|
+
async function generateAnthropicStyleMessage(provider, model, summary, maxLength, signal) {
|
|
140
|
+
const apiKey = getApiKey(provider);
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const headers = {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"x-api-key": apiKey,
|
|
147
|
+
"anthropic-version": "2023-06-01",
|
|
148
|
+
...(provider.headers ?? {}),
|
|
149
|
+
};
|
|
150
|
+
const response = await fetch(`${provider.baseUrl.replace(/\/$/, "")}/messages`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers,
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
model,
|
|
155
|
+
max_tokens: 120,
|
|
156
|
+
temperature: 0.2,
|
|
157
|
+
system: "Generate exactly one commit message line in format '<type>: <subject>'. Type must be feat, fix, or refector.",
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
role: "user",
|
|
161
|
+
content: buildUserPrompt(summary, maxLength),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}),
|
|
165
|
+
signal,
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const payload = (await response.json());
|
|
171
|
+
const firstText = payload.content?.find((item) => item.type === "text")?.text;
|
|
172
|
+
return firstText;
|
|
173
|
+
}
|
|
174
|
+
async function generateCommitMessage(ai, summary, maxLength) {
|
|
175
|
+
if (!ai.enabled) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
179
|
+
if (!provider || !model) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const providerConfig = ai.providers[provider];
|
|
183
|
+
if (!providerConfig) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timeout = setTimeout(() => controller.abort(), ai.timeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
let content;
|
|
190
|
+
if (providerConfig.api === "openai-completions") {
|
|
191
|
+
content = await generateOpenAiStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
content = await generateAnthropicStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
|
|
195
|
+
}
|
|
196
|
+
const normalized = normalizeMessage(content ?? "", maxLength);
|
|
197
|
+
return normalized || undefined;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AutoCommitConfig, LoadConfigOptions } from "../types";
|
|
2
|
+
export declare function resolveConfigPath(options: LoadConfigOptions): string;
|
|
3
|
+
export declare function loadConfig(options: LoadConfigOptions): {
|
|
4
|
+
config: AutoCommitConfig;
|
|
5
|
+
path: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function initConfigFile(targetPath: string, worktree: string): AutoCommitConfig;
|
|
8
|
+
export declare function updateConfigWorktree(configPath: string, worktree: string): AutoCommitConfig;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveConfigPath = resolveConfigPath;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.initConfigFile = initConfigFile;
|
|
9
|
+
exports.updateConfigWorktree = updateConfigWorktree;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const fs_1 = require("./fs");
|
|
13
|
+
const DEFAULT_CONFIG = (worktree) => ({
|
|
14
|
+
version: 1,
|
|
15
|
+
enabled: true,
|
|
16
|
+
worktree,
|
|
17
|
+
commit: {
|
|
18
|
+
mode: "single",
|
|
19
|
+
fallbackPrefix: "chore(auto)",
|
|
20
|
+
maxMessageLength: 72,
|
|
21
|
+
},
|
|
22
|
+
ai: {
|
|
23
|
+
enabled: false,
|
|
24
|
+
timeoutMs: 15000,
|
|
25
|
+
model: "openai/gpt-4.1-mini",
|
|
26
|
+
defaultProvider: "openai",
|
|
27
|
+
providers: {
|
|
28
|
+
openai: {
|
|
29
|
+
api: "openai-completions",
|
|
30
|
+
baseUrl: "https://api.openai.com/v1",
|
|
31
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
32
|
+
},
|
|
33
|
+
anthropic: {
|
|
34
|
+
api: "anthropic-messages",
|
|
35
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
36
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
37
|
+
},
|
|
38
|
+
openrouter: {
|
|
39
|
+
api: "openai-completions",
|
|
40
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
41
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
42
|
+
},
|
|
43
|
+
moonshot: {
|
|
44
|
+
api: "openai-completions",
|
|
45
|
+
baseUrl: "https://api.moonshot.ai/v1",
|
|
46
|
+
apiKeyEnv: "MOONSHOT_API_KEY",
|
|
47
|
+
},
|
|
48
|
+
minimax: {
|
|
49
|
+
api: "openai-completions",
|
|
50
|
+
baseUrl: "https://api.minimax.chat/v1",
|
|
51
|
+
apiKeyEnv: "MINIMAX_API_KEY",
|
|
52
|
+
},
|
|
53
|
+
"kimi-coding": {
|
|
54
|
+
api: "anthropic-messages",
|
|
55
|
+
baseUrl: "https://api.moonshot.ai/anthropic",
|
|
56
|
+
apiKeyEnv: "KIMI_API_KEY",
|
|
57
|
+
},
|
|
58
|
+
ollama: {
|
|
59
|
+
api: "openai-completions",
|
|
60
|
+
baseUrl: "http://127.0.0.1:11434/v1",
|
|
61
|
+
apiKeyEnv: "OLLAMA_API_KEY",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
push: {
|
|
66
|
+
enabled: false,
|
|
67
|
+
provider: "github",
|
|
68
|
+
remote: "origin",
|
|
69
|
+
branch: "",
|
|
70
|
+
},
|
|
71
|
+
filters: {
|
|
72
|
+
include: [],
|
|
73
|
+
exclude: [".env", ".env.*", "*.pem", "*.key", "*.p12"],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
function mergeConfig(base, override) {
|
|
77
|
+
const mergedAiProviders = {
|
|
78
|
+
...base.ai.providers,
|
|
79
|
+
};
|
|
80
|
+
const overrideProviders = override.ai?.providers ?? {};
|
|
81
|
+
for (const [providerName, providerConfig] of Object.entries(overrideProviders)) {
|
|
82
|
+
mergedAiProviders[providerName] = {
|
|
83
|
+
...(base.ai.providers[providerName] ?? {}),
|
|
84
|
+
...providerConfig,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
...override,
|
|
90
|
+
commit: {
|
|
91
|
+
...base.commit,
|
|
92
|
+
...override.commit,
|
|
93
|
+
},
|
|
94
|
+
ai: {
|
|
95
|
+
...base.ai,
|
|
96
|
+
...override.ai,
|
|
97
|
+
providers: mergedAiProviders,
|
|
98
|
+
},
|
|
99
|
+
push: {
|
|
100
|
+
...base.push,
|
|
101
|
+
...override.push,
|
|
102
|
+
},
|
|
103
|
+
filters: {
|
|
104
|
+
...base.filters,
|
|
105
|
+
...override.filters,
|
|
106
|
+
include: override.filters?.include ?? base.filters.include,
|
|
107
|
+
exclude: override.filters?.exclude ?? base.filters.exclude,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function normalizeConfig(config) {
|
|
112
|
+
if (config.commit.mode !== "single" && config.commit.mode !== "per-file") {
|
|
113
|
+
throw new Error(`Invalid commit.mode: ${config.commit.mode}`);
|
|
114
|
+
}
|
|
115
|
+
if (config.push.provider !== "github" && config.push.provider !== "gitlab" && config.push.provider !== "generic") {
|
|
116
|
+
throw new Error(`Invalid push.provider: ${config.push.provider}`);
|
|
117
|
+
}
|
|
118
|
+
if (config.commit.maxMessageLength < 20) {
|
|
119
|
+
config.commit.maxMessageLength = 20;
|
|
120
|
+
}
|
|
121
|
+
if (config.ai.timeoutMs < 1000) {
|
|
122
|
+
config.ai.timeoutMs = 1000;
|
|
123
|
+
}
|
|
124
|
+
if (!config.ai.model.trim()) {
|
|
125
|
+
config.ai.model = "openai/gpt-4.1-mini";
|
|
126
|
+
}
|
|
127
|
+
if (!config.ai.defaultProvider.trim()) {
|
|
128
|
+
config.ai.defaultProvider = "openai";
|
|
129
|
+
}
|
|
130
|
+
const allowedApis = new Set(["openai-completions", "anthropic-messages"]);
|
|
131
|
+
for (const [provider, providerConfig] of Object.entries(config.ai.providers)) {
|
|
132
|
+
if (!providerConfig.baseUrl?.trim()) {
|
|
133
|
+
throw new Error(`Invalid ai.providers.${provider}.baseUrl`);
|
|
134
|
+
}
|
|
135
|
+
if (!allowedApis.has(providerConfig.api)) {
|
|
136
|
+
throw new Error(`Invalid ai.providers.${provider}.api: ${providerConfig.api}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!config.ai.providers[config.ai.defaultProvider]) {
|
|
140
|
+
throw new Error(`Missing ai.providers.${config.ai.defaultProvider}`);
|
|
141
|
+
}
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
function resolveConfigPath(options) {
|
|
145
|
+
if (options.explicitPath) {
|
|
146
|
+
return node_path_1.default.resolve(options.explicitPath);
|
|
147
|
+
}
|
|
148
|
+
const cwd = node_path_1.default.resolve(options.worktree ?? process.cwd());
|
|
149
|
+
const projectPath = (0, fs_1.getProjectConfigPath)(cwd);
|
|
150
|
+
if (node_fs_1.default.existsSync(projectPath)) {
|
|
151
|
+
return projectPath;
|
|
152
|
+
}
|
|
153
|
+
return (0, fs_1.getGlobalConfigPath)();
|
|
154
|
+
}
|
|
155
|
+
function loadConfig(options) {
|
|
156
|
+
const cwd = node_path_1.default.resolve(options.worktree ?? process.cwd());
|
|
157
|
+
const configPath = resolveConfigPath(options);
|
|
158
|
+
const raw = (0, fs_1.readJsonFile)(configPath);
|
|
159
|
+
const merged = mergeConfig(DEFAULT_CONFIG(cwd), raw ?? {});
|
|
160
|
+
if (!merged.worktree) {
|
|
161
|
+
merged.worktree = cwd;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
config: normalizeConfig(merged),
|
|
165
|
+
path: configPath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function initConfigFile(targetPath, worktree) {
|
|
169
|
+
const config = DEFAULT_CONFIG(node_path_1.default.resolve(worktree));
|
|
170
|
+
(0, fs_1.writeJsonFile)(targetPath, config);
|
|
171
|
+
return config;
|
|
172
|
+
}
|
|
173
|
+
function updateConfigWorktree(configPath, worktree) {
|
|
174
|
+
const resolved = node_path_1.default.resolve(worktree);
|
|
175
|
+
const raw = (0, fs_1.readJsonFile)(configPath) ?? {};
|
|
176
|
+
const merged = mergeConfig(DEFAULT_CONFIG(resolved), raw);
|
|
177
|
+
merged.worktree = resolved;
|
|
178
|
+
const normalized = normalizeConfig(merged);
|
|
179
|
+
(0, fs_1.writeJsonFile)(configPath, normalized);
|
|
180
|
+
return normalized;
|
|
181
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runCommand(command: string, args: string[], cwd: string): CommandResult;
|
|
7
|
+
export declare function runCommandOrThrow(command: string, args: string[], cwd: string): string;
|