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.
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runCommand = runCommand;
4
+ exports.runCommandOrThrow = runCommandOrThrow;
5
+ const node_child_process_1 = require("node:child_process");
6
+ function runCommand(command, args, cwd) {
7
+ const result = (0, node_child_process_1.spawnSync)(command, args, {
8
+ cwd,
9
+ encoding: "utf8",
10
+ stdio: ["ignore", "pipe", "pipe"],
11
+ });
12
+ return {
13
+ exitCode: result.status ?? 1,
14
+ stdout: result.stdout ?? "",
15
+ stderr: result.stderr ?? "",
16
+ };
17
+ }
18
+ function runCommandOrThrow(command, args, cwd) {
19
+ const result = runCommand(command, args, cwd);
20
+ if (result.exitCode !== 0) {
21
+ throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr.trim() || "unknown error"}`);
22
+ }
23
+ return result.stdout;
24
+ }
@@ -0,0 +1,2 @@
1
+ export declare function matchesAnyPattern(value: string, patterns: string[]): boolean;
2
+ export declare function shouldIncludePath(value: string, include: string[], exclude: string[]): boolean;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesAnyPattern = matchesAnyPattern;
4
+ exports.shouldIncludePath = shouldIncludePath;
5
+ function escapeRegex(value) {
6
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
7
+ }
8
+ function patternToRegex(pattern) {
9
+ let source = "^";
10
+ for (let i = 0; i < pattern.length; i += 1) {
11
+ const ch = pattern[i];
12
+ if (ch === "*") {
13
+ if (pattern[i + 1] === "*") {
14
+ source += ".*";
15
+ i += 1;
16
+ }
17
+ else {
18
+ source += "[^/]*";
19
+ }
20
+ }
21
+ else if (ch === "?") {
22
+ source += ".";
23
+ }
24
+ else {
25
+ source += escapeRegex(ch);
26
+ }
27
+ }
28
+ source += "$";
29
+ return new RegExp(source);
30
+ }
31
+ function matchesAnyPattern(value, patterns) {
32
+ if (patterns.length === 0) {
33
+ return false;
34
+ }
35
+ return patterns.some((pattern) => patternToRegex(pattern).test(value));
36
+ }
37
+ function shouldIncludePath(value, include, exclude) {
38
+ if (include.length > 0 && !matchesAnyPattern(value, include)) {
39
+ return false;
40
+ }
41
+ if (exclude.length > 0 && matchesAnyPattern(value, exclude)) {
42
+ return false;
43
+ }
44
+ return true;
45
+ }
@@ -0,0 +1,7 @@
1
+ export declare function getUserConfigHome(): string;
2
+ export declare function getProjectConfigPath(worktree: string): string;
3
+ export declare function getGlobalConfigPath(): string;
4
+ export declare function ensureDirForFile(filePath: string): void;
5
+ export declare function readJsonFile<T>(filePath: string): T | undefined;
6
+ export declare function writeJsonFile(filePath: string, value: unknown): void;
7
+ export declare function writeTextFile(filePath: string, content: string): void;
@@ -0,0 +1,46 @@
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.getUserConfigHome = getUserConfigHome;
7
+ exports.getProjectConfigPath = getProjectConfigPath;
8
+ exports.getGlobalConfigPath = getGlobalConfigPath;
9
+ exports.ensureDirForFile = ensureDirForFile;
10
+ exports.readJsonFile = readJsonFile;
11
+ exports.writeJsonFile = writeJsonFile;
12
+ exports.writeTextFile = writeTextFile;
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ function getUserConfigHome() {
17
+ const xdg = process.env.XDG_CONFIG_HOME;
18
+ if (xdg && xdg.trim().length > 0) {
19
+ return xdg;
20
+ }
21
+ return node_path_1.default.join(node_os_1.default.homedir(), ".config");
22
+ }
23
+ function getProjectConfigPath(worktree) {
24
+ return node_path_1.default.join(worktree, ".code-agent-auto-commit.json");
25
+ }
26
+ function getGlobalConfigPath() {
27
+ return node_path_1.default.join(getUserConfigHome(), "code-agent-auto-commit", "config.json");
28
+ }
29
+ function ensureDirForFile(filePath) {
30
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
31
+ }
32
+ function readJsonFile(filePath) {
33
+ if (!node_fs_1.default.existsSync(filePath)) {
34
+ return undefined;
35
+ }
36
+ const content = node_fs_1.default.readFileSync(filePath, "utf8");
37
+ return JSON.parse(content);
38
+ }
39
+ function writeJsonFile(filePath, value) {
40
+ ensureDirForFile(filePath);
41
+ node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
42
+ }
43
+ function writeTextFile(filePath, content) {
44
+ ensureDirForFile(filePath);
45
+ node_fs_1.default.writeFileSync(filePath, content, "utf8");
46
+ }
@@ -0,0 +1,10 @@
1
+ import type { ChangedFile, CommitSummary, PushProvider } from "../types";
2
+ export declare function ensureGitRepository(worktree: string): void;
3
+ export declare function listChangedFiles(worktree: string): ChangedFile[];
4
+ export declare function hasStagedChanges(worktree: string): boolean;
5
+ export declare function stagePath(worktree: string, filePath: string): void;
6
+ export declare function commit(worktree: string, message: string): string;
7
+ export declare function getStagedSummary(worktree: string, onlyPath?: string): CommitSummary;
8
+ export declare function getCurrentBranch(worktree: string): string;
9
+ export declare function getRemoteUrl(worktree: string, remote: string): string;
10
+ export declare function push(worktree: string, remote: string, branch: string, provider: PushProvider): void;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureGitRepository = ensureGitRepository;
4
+ exports.listChangedFiles = listChangedFiles;
5
+ exports.hasStagedChanges = hasStagedChanges;
6
+ exports.stagePath = stagePath;
7
+ exports.commit = commit;
8
+ exports.getStagedSummary = getStagedSummary;
9
+ exports.getCurrentBranch = getCurrentBranch;
10
+ exports.getRemoteUrl = getRemoteUrl;
11
+ exports.push = push;
12
+ const exec_1 = require("./exec");
13
+ function assertGitRepo(worktree) {
14
+ const result = (0, exec_1.runCommand)("git", ["-C", worktree, "rev-parse", "--is-inside-work-tree"], worktree);
15
+ if (result.exitCode !== 0 || result.stdout.trim() !== "true") {
16
+ throw new Error(`Not a git repository: ${worktree}`);
17
+ }
18
+ }
19
+ function ensureGitRepository(worktree) {
20
+ assertGitRepo(worktree);
21
+ }
22
+ function listChangedFiles(worktree) {
23
+ assertGitRepo(worktree);
24
+ const output = (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "status", "--porcelain", "-z"], worktree);
25
+ if (!output) {
26
+ return [];
27
+ }
28
+ const files = [];
29
+ let offset = 0;
30
+ while (offset < output.length) {
31
+ const statusPair = output.slice(offset, offset + 2);
32
+ offset += 3;
33
+ const firstNull = output.indexOf("\0", offset);
34
+ if (firstNull === -1) {
35
+ break;
36
+ }
37
+ const firstPath = output.slice(offset, firstNull);
38
+ offset = firstNull + 1;
39
+ const indexStatus = statusPair[0];
40
+ const worktreeStatus = statusPair[1];
41
+ const isRenameOrCopy = indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C";
42
+ if (isRenameOrCopy) {
43
+ const secondNull = output.indexOf("\0", offset);
44
+ if (secondNull === -1) {
45
+ break;
46
+ }
47
+ const secondPath = output.slice(offset, secondNull);
48
+ offset = secondNull + 1;
49
+ files.push({
50
+ path: secondPath,
51
+ originalPath: firstPath,
52
+ indexStatus,
53
+ worktreeStatus,
54
+ });
55
+ }
56
+ else {
57
+ files.push({
58
+ path: firstPath,
59
+ indexStatus,
60
+ worktreeStatus,
61
+ });
62
+ }
63
+ }
64
+ return files;
65
+ }
66
+ function hasStagedChanges(worktree) {
67
+ const result = (0, exec_1.runCommand)("git", ["-C", worktree, "diff", "--cached", "--quiet"], worktree);
68
+ return result.exitCode !== 0;
69
+ }
70
+ function stagePath(worktree, filePath) {
71
+ (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "add", "-A", "--", filePath], worktree);
72
+ }
73
+ function commit(worktree, message) {
74
+ (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "commit", "-m", message], worktree);
75
+ return (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "rev-parse", "HEAD"], worktree).trim();
76
+ }
77
+ function getStagedSummary(worktree, onlyPath) {
78
+ const pathArgs = onlyPath ? ["--", onlyPath] : [];
79
+ const nameStatus = (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "diff", "--cached", "--name-status", ...pathArgs], worktree).trim();
80
+ const diffStat = (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "diff", "--cached", "--stat", ...pathArgs], worktree).trim();
81
+ const patchRaw = (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "diff", "--cached", ...pathArgs], worktree);
82
+ const patch = patchRaw.slice(0, 12000);
83
+ return {
84
+ nameStatus,
85
+ diffStat,
86
+ patch,
87
+ };
88
+ }
89
+ function getCurrentBranch(worktree) {
90
+ return (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "rev-parse", "--abbrev-ref", "HEAD"], worktree).trim();
91
+ }
92
+ function getRemoteUrl(worktree, remote) {
93
+ return (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "remote", "get-url", remote], worktree).trim();
94
+ }
95
+ function validateProvider(provider, remoteUrl) {
96
+ const lower = remoteUrl.toLowerCase();
97
+ if (provider === "github" && !lower.includes("github")) {
98
+ throw new Error(`Remote URL does not look like GitHub: ${remoteUrl}`);
99
+ }
100
+ if (provider === "gitlab" && !lower.includes("gitlab")) {
101
+ throw new Error(`Remote URL does not look like GitLab: ${remoteUrl}`);
102
+ }
103
+ }
104
+ function push(worktree, remote, branch, provider) {
105
+ const remoteUrl = getRemoteUrl(worktree, remote);
106
+ validateProvider(provider, remoteUrl);
107
+ (0, exec_1.runCommandOrThrow)("git", ["-C", worktree, "push", remote, branch], worktree);
108
+ }
@@ -0,0 +1,2 @@
1
+ import type { LoadConfigOptions, RunContext, RunResult } from "../types";
2
+ export declare function runAutoCommit(context: RunContext, configOptions: LoadConfigOptions): Promise<RunResult>;
@@ -0,0 +1,148 @@
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.runAutoCommit = runAutoCommit;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const ai_1 = require("./ai");
9
+ const config_1 = require("./config");
10
+ const filter_1 = require("./filter");
11
+ const git_1 = require("./git");
12
+ function normalizeFallbackType(prefix) {
13
+ const value = prefix.toLowerCase();
14
+ if (/(^|[^a-z])(feat|feature)([^a-z]|$)/.test(value)) {
15
+ return "feat";
16
+ }
17
+ if (/(^|[^a-z])(fix|bugfix|hotfix)([^a-z]|$)/.test(value)) {
18
+ return "fix";
19
+ }
20
+ if (/(^|[^a-z])(refector|refactor|chore|docs|style|test|perf|build|ci|revert)([^a-z]|$)/.test(value)) {
21
+ return "refector";
22
+ }
23
+ return "refector";
24
+ }
25
+ function fallbackSingleMessage(prefix, count) {
26
+ const suffix = count === 1 ? "file" : "files";
27
+ return `${normalizeFallbackType(prefix)}: update ${count} ${suffix}`;
28
+ }
29
+ function fallbackPerFileMessage(prefix, file) {
30
+ const type = normalizeFallbackType(prefix);
31
+ const primary = file.indexStatus !== " " ? file.indexStatus : file.worktreeStatus;
32
+ const name = node_path_1.default.basename(file.path);
33
+ if (primary === "A") {
34
+ return `${type}: add ${name}`;
35
+ }
36
+ if (primary === "D") {
37
+ return `${type}: remove ${name}`;
38
+ }
39
+ if (primary === "R") {
40
+ return `${type}: rename ${name}`;
41
+ }
42
+ return `${type}: update ${name}`;
43
+ }
44
+ function uniqueSorted(files) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ for (const file of files) {
48
+ if (seen.has(file.path)) {
49
+ continue;
50
+ }
51
+ seen.add(file.path);
52
+ out.push(file);
53
+ }
54
+ out.sort((a, b) => a.path.localeCompare(b.path));
55
+ return out;
56
+ }
57
+ function filterFiles(files, include, exclude) {
58
+ return files.filter((file) => (0, filter_1.shouldIncludePath)(file.path, include, exclude));
59
+ }
60
+ async function buildMessage(prefix, maxLength, aiConfig, stagedPath, fallback, worktree) {
61
+ const summary = (0, git_1.getStagedSummary)(worktree, stagedPath);
62
+ const generated = await (0, ai_1.generateCommitMessage)(aiConfig, summary, maxLength);
63
+ if (generated) {
64
+ return generated;
65
+ }
66
+ if (fallback.length <= maxLength) {
67
+ return fallback;
68
+ }
69
+ return `${normalizeFallbackType(prefix)}: update changes`;
70
+ }
71
+ async function runAutoCommit(context, configOptions) {
72
+ const { config } = (0, config_1.loadConfig)(configOptions);
73
+ const worktree = node_path_1.default.resolve(context.worktree ?? config.worktree);
74
+ if (!config.enabled) {
75
+ return {
76
+ skipped: true,
77
+ reason: "disabled",
78
+ worktree,
79
+ committed: [],
80
+ pushed: false,
81
+ };
82
+ }
83
+ (0, git_1.ensureGitRepository)(worktree);
84
+ const changed = uniqueSorted(filterFiles((0, git_1.listChangedFiles)(worktree), config.filters.include, config.filters.exclude));
85
+ if (changed.length === 0) {
86
+ return {
87
+ skipped: true,
88
+ reason: "no changes",
89
+ worktree,
90
+ committed: [],
91
+ pushed: false,
92
+ };
93
+ }
94
+ const commits = [];
95
+ if (config.commit.mode === "single") {
96
+ for (const file of changed) {
97
+ (0, git_1.stagePath)(worktree, file.path);
98
+ }
99
+ if (!(0, git_1.hasStagedChanges)(worktree)) {
100
+ return {
101
+ skipped: true,
102
+ reason: "no staged changes",
103
+ worktree,
104
+ committed: [],
105
+ pushed: false,
106
+ };
107
+ }
108
+ const fallback = fallbackSingleMessage(config.commit.fallbackPrefix, changed.length);
109
+ const message = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, undefined, fallback, worktree);
110
+ const hash = (0, git_1.commit)(worktree, message);
111
+ commits.push({
112
+ hash,
113
+ message,
114
+ files: changed.map((item) => item.path),
115
+ });
116
+ }
117
+ else {
118
+ if ((0, git_1.hasStagedChanges)(worktree)) {
119
+ throw new Error("per-file mode requires a clean staging area before auto-commit");
120
+ }
121
+ for (const file of changed) {
122
+ (0, git_1.stagePath)(worktree, file.path);
123
+ if (!(0, git_1.hasStagedChanges)(worktree)) {
124
+ continue;
125
+ }
126
+ const fallback = fallbackPerFileMessage(config.commit.fallbackPrefix, file);
127
+ const message = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, file.path, fallback, worktree);
128
+ const hash = (0, git_1.commit)(worktree, message);
129
+ commits.push({
130
+ hash,
131
+ message,
132
+ files: [file.path],
133
+ });
134
+ }
135
+ }
136
+ let pushed = false;
137
+ if (commits.length > 0 && config.push.enabled) {
138
+ const branch = config.push.branch || (0, git_1.getCurrentBranch)(worktree);
139
+ (0, git_1.push)(worktree, config.push.remote, branch, config.push.provider);
140
+ pushed = true;
141
+ }
142
+ return {
143
+ skipped: false,
144
+ worktree,
145
+ committed: commits,
146
+ pushed,
147
+ };
148
+ }
@@ -0,0 +1,6 @@
1
+ export { loadConfig, initConfigFile, resolveConfigPath, updateConfigWorktree } from "./core/config";
2
+ export { runAutoCommit } from "./core/run";
3
+ export { installOpenCodeAdapter, uninstallOpenCodeAdapter, opencodeAdapterStatus } from "./adapters/opencode";
4
+ export { installCodexAdapter, uninstallCodexAdapter, codexAdapterStatus } from "./adapters/codex";
5
+ export { installClaudeAdapter, uninstallClaudeAdapter, claudeAdapterStatus } from "./adapters/claude";
6
+ export type { AutoCommitConfig, CommitMode, PushProvider, ToolName, InstallScope, RunResult, RunContext, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeAdapterStatus = exports.uninstallClaudeAdapter = exports.installClaudeAdapter = exports.codexAdapterStatus = exports.uninstallCodexAdapter = exports.installCodexAdapter = exports.opencodeAdapterStatus = exports.uninstallOpenCodeAdapter = exports.installOpenCodeAdapter = exports.runAutoCommit = exports.updateConfigWorktree = exports.resolveConfigPath = exports.initConfigFile = exports.loadConfig = void 0;
4
+ var config_1 = require("./core/config");
5
+ Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
6
+ Object.defineProperty(exports, "initConfigFile", { enumerable: true, get: function () { return config_1.initConfigFile; } });
7
+ Object.defineProperty(exports, "resolveConfigPath", { enumerable: true, get: function () { return config_1.resolveConfigPath; } });
8
+ Object.defineProperty(exports, "updateConfigWorktree", { enumerable: true, get: function () { return config_1.updateConfigWorktree; } });
9
+ var run_1 = require("./core/run");
10
+ Object.defineProperty(exports, "runAutoCommit", { enumerable: true, get: function () { return run_1.runAutoCommit; } });
11
+ var opencode_1 = require("./adapters/opencode");
12
+ Object.defineProperty(exports, "installOpenCodeAdapter", { enumerable: true, get: function () { return opencode_1.installOpenCodeAdapter; } });
13
+ Object.defineProperty(exports, "uninstallOpenCodeAdapter", { enumerable: true, get: function () { return opencode_1.uninstallOpenCodeAdapter; } });
14
+ Object.defineProperty(exports, "opencodeAdapterStatus", { enumerable: true, get: function () { return opencode_1.opencodeAdapterStatus; } });
15
+ var codex_1 = require("./adapters/codex");
16
+ Object.defineProperty(exports, "installCodexAdapter", { enumerable: true, get: function () { return codex_1.installCodexAdapter; } });
17
+ Object.defineProperty(exports, "uninstallCodexAdapter", { enumerable: true, get: function () { return codex_1.uninstallCodexAdapter; } });
18
+ Object.defineProperty(exports, "codexAdapterStatus", { enumerable: true, get: function () { return codex_1.codexAdapterStatus; } });
19
+ var claude_1 = require("./adapters/claude");
20
+ Object.defineProperty(exports, "installClaudeAdapter", { enumerable: true, get: function () { return claude_1.installClaudeAdapter; } });
21
+ Object.defineProperty(exports, "uninstallClaudeAdapter", { enumerable: true, get: function () { return claude_1.uninstallClaudeAdapter; } });
22
+ Object.defineProperty(exports, "claudeAdapterStatus", { enumerable: true, get: function () { return claude_1.claudeAdapterStatus; } });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
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
+ const node_fs_1 = __importDefault(require("node:fs"));
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_test_1 = __importDefault(require("node:test"));
10
+ const strict_1 = __importDefault(require("node:assert/strict"));
11
+ const config_1 = require("../core/config");
12
+ (0, node_test_1.default)("init and update config file", () => {
13
+ const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "code-agent-auto-commit-"));
14
+ const configPath = node_path_1.default.join(tempDir, ".code-agent-auto-commit.json");
15
+ const created = (0, config_1.initConfigFile)(configPath, tempDir);
16
+ strict_1.default.equal(created.version, 1);
17
+ strict_1.default.equal(created.worktree, tempDir);
18
+ const loaded = (0, config_1.loadConfig)({ explicitPath: configPath, worktree: tempDir });
19
+ strict_1.default.equal(loaded.config.worktree, tempDir);
20
+ const nextDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "code-agent-auto-commit-next-"));
21
+ const updated = (0, config_1.updateConfigWorktree)(configPath, nextDir);
22
+ strict_1.default.equal(updated.worktree, nextDir);
23
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
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
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const filter_1 = require("../core/filter");
9
+ (0, node_test_1.default)("include and exclude patterns are applied", () => {
10
+ strict_1.default.equal((0, filter_1.shouldIncludePath)("src/app.ts", ["src/**"], []), true);
11
+ strict_1.default.equal((0, filter_1.shouldIncludePath)("README.md", ["src/**"], []), false);
12
+ strict_1.default.equal((0, filter_1.shouldIncludePath)(".env", [], [".env", ".env.*"]), false);
13
+ strict_1.default.equal((0, filter_1.shouldIncludePath)("src/main.ts", [], ["*.md"]), true);
14
+ });
@@ -0,0 +1,76 @@
1
+ export type CommitMode = "single" | "per-file";
2
+ export type PushProvider = "github" | "gitlab" | "generic";
3
+ export type ToolName = "opencode" | "codex" | "claude" | "manual";
4
+ export type InstallScope = "project" | "global";
5
+ export type AIProviderApi = "openai-completions" | "anthropic-messages";
6
+ export interface CommitConfig {
7
+ mode: CommitMode;
8
+ fallbackPrefix: string;
9
+ maxMessageLength: number;
10
+ }
11
+ export interface AIConfig {
12
+ enabled: boolean;
13
+ timeoutMs: number;
14
+ model: string;
15
+ defaultProvider: string;
16
+ providers: Record<string, AIProviderConfig>;
17
+ }
18
+ export interface AIProviderConfig {
19
+ api: AIProviderApi;
20
+ baseUrl: string;
21
+ apiKey?: string;
22
+ apiKeyEnv?: string;
23
+ headers?: Record<string, string>;
24
+ }
25
+ export interface PushConfig {
26
+ enabled: boolean;
27
+ provider: PushProvider;
28
+ remote: string;
29
+ branch: string;
30
+ }
31
+ export interface FilterConfig {
32
+ include: string[];
33
+ exclude: string[];
34
+ }
35
+ export interface AutoCommitConfig {
36
+ version: 1;
37
+ enabled: boolean;
38
+ worktree: string;
39
+ commit: CommitConfig;
40
+ ai: AIConfig;
41
+ push: PushConfig;
42
+ filters: FilterConfig;
43
+ }
44
+ export interface ChangedFile {
45
+ path: string;
46
+ originalPath?: string;
47
+ indexStatus: string;
48
+ worktreeStatus: string;
49
+ }
50
+ export interface CommitSummary {
51
+ nameStatus: string;
52
+ diffStat: string;
53
+ patch: string;
54
+ }
55
+ export interface CommitRecord {
56
+ hash: string;
57
+ message: string;
58
+ files: string[];
59
+ }
60
+ export interface RunResult {
61
+ skipped: boolean;
62
+ reason?: string;
63
+ worktree: string;
64
+ committed: CommitRecord[];
65
+ pushed: boolean;
66
+ }
67
+ export interface RunContext {
68
+ tool: ToolName;
69
+ worktree?: string;
70
+ sessionID?: string;
71
+ event?: unknown;
72
+ }
73
+ export interface LoadConfigOptions {
74
+ explicitPath?: string;
75
+ worktree?: string;
76
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/docs/CONFIG.md ADDED
@@ -0,0 +1,72 @@
1
+ # Configuration Reference
2
+
3
+ `cac` reads JSON config from:
4
+
5
+ 1. `--config <path>` if provided
6
+ 2. `<worktree>/.code-agent-auto-commit.json` if it exists
7
+ 3. `~/.config/code-agent-auto-commit/config.json`
8
+
9
+ ## Schema
10
+
11
+ ```json
12
+ {
13
+ "version": 1,
14
+ "enabled": true,
15
+ "worktree": "/absolute/path",
16
+ "commit": {
17
+ "mode": "single",
18
+ "fallbackPrefix": "chore(auto)",
19
+ "maxMessageLength": 72
20
+ },
21
+ "ai": {
22
+ "enabled": false,
23
+ "timeoutMs": 15000,
24
+ "model": "openai/gpt-4.1-mini",
25
+ "defaultProvider": "openai",
26
+ "providers": {
27
+ "openai": {
28
+ "api": "openai-completions",
29
+ "baseUrl": "https://api.openai.com/v1",
30
+ "apiKeyEnv": "OPENAI_API_KEY"
31
+ },
32
+ "anthropic": {
33
+ "api": "anthropic-messages",
34
+ "baseUrl": "https://api.anthropic.com/v1",
35
+ "apiKeyEnv": "ANTHROPIC_API_KEY"
36
+ }
37
+ }
38
+ },
39
+ "push": {
40
+ "enabled": false,
41
+ "provider": "github",
42
+ "remote": "origin",
43
+ "branch": ""
44
+ },
45
+ "filters": {
46
+ "include": [],
47
+ "exclude": [".env", ".env.*", "*.pem", "*.key", "*.p12"]
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## AI Provider Config (OpenClaw-style)
53
+
54
+ - `ai.model`: supports `provider/model` format (example: `openai/gpt-4.1-mini`, `anthropic/claude-3-5-sonnet-latest`).
55
+ - `ai.defaultProvider`: used when `ai.model` does not include a provider prefix.
56
+ - `ai.providers.<name>.api`:
57
+ - `openai-completions` -> `POST /chat/completions`
58
+ - `anthropic-messages` -> `POST /messages`
59
+ - `ai.providers.<name>.baseUrl`: provider endpoint base URL.
60
+ - `ai.providers.<name>.apiKey` or `apiKeyEnv`: API key source (env preferred).
61
+ - `ai.providers.<name>.headers`: optional custom headers.
62
+
63
+ ## Notes
64
+
65
+ - `commit.mode`
66
+ - `single`: stage and commit all selected files together
67
+ - `per-file`: commit each selected file separately
68
+ - `push.provider`
69
+ - `github`: remote URL must contain `github`
70
+ - `gitlab`: remote URL must contain `gitlab`
71
+ - `generic`: no provider URL validation
72
+ - Keep API keys in environment variables when possible.