@synmux/claude-commit 1.0.3 → 1.1.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/CHANGELOG.md +238 -0
- package/README.md +92 -38
- package/bin/cco.js +16 -0
- package/bin/cco.ts +3 -6
- package/dist/bin/cco.js +2347 -0
- package/dist/index.js +1642 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/src/agent.d.ts +93 -0
- package/dist/types/src/config.d.ts +36 -0
- package/dist/types/src/diff.d.ts +115 -0
- package/{src/errors.ts → dist/types/src/errors.d.ts} +3 -6
- package/dist/types/src/generate.d.ts +113 -0
- package/dist/types/src/git.d.ts +31 -0
- package/dist/types/src/models.d.ts +42 -0
- package/dist/types/src/ollama.d.ts +89 -0
- package/dist/types/src/paths.d.ts +9 -0
- package/dist/types/src/prompts.d.ts +59 -0
- package/dist/types/src/tokens.d.ts +58 -0
- package/dist/types/src/types.d.ts +234 -0
- package/{src/ui/colors.ts → dist/types/src/ui/colors.d.ts} +2 -5
- package/dist/types/src/ui/spinner.d.ts +23 -0
- package/package.json +45 -32
- package/index.ts +0 -69
- package/src/agent.ts +0 -280
- package/src/cli.ts +0 -442
- package/src/config.ts +0 -337
- package/src/diff.ts +0 -571
- package/src/generate.ts +0 -478
- package/src/git.ts +0 -145
- package/src/models.ts +0 -95
- package/src/ollama.ts +0 -502
- package/src/paths.ts +0 -139
- package/src/prompts.ts +0 -364
- package/src/tokens.ts +0 -147
- package/src/types.ts +0 -238
- package/src/ui/editor.ts +0 -89
- package/src/ui/interactive.ts +0 -313
- package/src/ui/spinner.ts +0 -79
- package/src/utils.ts +0 -5
package/src/config.ts
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration loading and merging.
|
|
3
|
-
*
|
|
4
|
-
* Precedence (low to high): built-in defaults < a global user config in
|
|
5
|
-
* `$XDG_CONFIG_HOME/claude-commit` (default `~/.config/claude-commit`) <
|
|
6
|
-
* `package.json` (`claude-commit` key at the repo root) < the nearest project
|
|
7
|
-
* `.claude-commit.json` / `.claude-commitrc(.json)` file (searched cwd → repo
|
|
8
|
-
* root) < CLI flags.
|
|
9
|
-
*/
|
|
10
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { ClaudeCommitError } from "./errors";
|
|
13
|
-
import { DEFAULT_SPINNER, isSpinnerName } from "./ui/spinner";
|
|
14
|
-
import { DEFAULT_OLLAMA_CONTEXT, DEFAULT_OLLAMA_HOST } from "./models";
|
|
15
|
-
import type { Config, ModelConfig, OllamaConfig, PartialConfig } from "./types";
|
|
16
|
-
|
|
17
|
-
export const DEFAULT_CONFIG: Config = {
|
|
18
|
-
conventionalCommits: false,
|
|
19
|
-
gitmoji: false,
|
|
20
|
-
multiline: false,
|
|
21
|
-
template: null,
|
|
22
|
-
customPrompt: null,
|
|
23
|
-
interactive: false,
|
|
24
|
-
interactiveCount: 3,
|
|
25
|
-
interactiveTemperature: 1,
|
|
26
|
-
spinner: DEFAULT_SPINNER,
|
|
27
|
-
models: {
|
|
28
|
-
summary: "sonnet",
|
|
29
|
-
final: "sonnet",
|
|
30
|
-
},
|
|
31
|
-
maxChunkTokens: 600_000,
|
|
32
|
-
charsPerToken: 3.5,
|
|
33
|
-
skipArmored: false,
|
|
34
|
-
lowPriorityPaths: [],
|
|
35
|
-
ignore: [],
|
|
36
|
-
ollama: {
|
|
37
|
-
host: DEFAULT_OLLAMA_HOST,
|
|
38
|
-
context: DEFAULT_OLLAMA_CONTEXT,
|
|
39
|
-
keepAlive: null,
|
|
40
|
-
},
|
|
41
|
-
allowApiKey: false,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const CONFIG_FILENAMES = [
|
|
45
|
-
".claude-commit.json",
|
|
46
|
-
".claude-commitrc.json",
|
|
47
|
-
".claude-commitrc",
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Filenames accepted inside the global config directory, most-preferred first.
|
|
52
|
-
* `config.json` is the canonical name (the directory already says which tool it
|
|
53
|
-
* is for); the project-style names are also honoured so a config can be copied
|
|
54
|
-
* or symlinked there.
|
|
55
|
-
*/
|
|
56
|
-
const GLOBAL_CONFIG_FILENAMES = ["config.json", ...CONFIG_FILENAMES];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The user-level config directory, `$XDG_CONFIG_HOME/claude-commit` (falling back
|
|
60
|
-
* to `~/.config/claude-commit`). Per the XDG Base Directory spec, `XDG_CONFIG_HOME`
|
|
61
|
-
* is honoured only when it is set to an absolute path.
|
|
62
|
-
*/
|
|
63
|
-
export function globalConfigDir(
|
|
64
|
-
env: Record<string, string | undefined> = process.env,
|
|
65
|
-
): string {
|
|
66
|
-
const xdg = env.XDG_CONFIG_HOME;
|
|
67
|
-
const base = xdg && isAbsolute(xdg) ? xdg : join(homedir(), ".config");
|
|
68
|
-
return join(base, "claude-commit");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** The first existing global config file in {@link globalConfigDir}, if any. */
|
|
72
|
-
async function findGlobalConfigFile(
|
|
73
|
-
env: Record<string, string | undefined> = process.env,
|
|
74
|
-
): Promise<string | undefined> {
|
|
75
|
-
const dir = globalConfigDir(env);
|
|
76
|
-
for (const name of GLOBAL_CONFIG_FILENAMES) {
|
|
77
|
-
const candidate = join(dir, name);
|
|
78
|
-
if (await Bun.file(candidate).exists()) return candidate;
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Deep-ish merge of a partial config over a base config: `models` and
|
|
85
|
-
* `ollama` are merged key by key; the path lists (`lowPriorityPaths`,
|
|
86
|
-
* `ignore`) are replaced whole - a higher layer's list wins outright, so a
|
|
87
|
-
* project can drop a global pattern - and copied so the result never
|
|
88
|
-
* aliases the base's array.
|
|
89
|
-
*/
|
|
90
|
-
export function mergeConfig(base: Config, override: PartialConfig): Config {
|
|
91
|
-
const models: ModelConfig = { ...base.models, ...(override.models ?? {}) };
|
|
92
|
-
const ollama: OllamaConfig = { ...base.ollama, ...(override.ollama ?? {}) };
|
|
93
|
-
const lowPriorityPaths = [
|
|
94
|
-
...(override.lowPriorityPaths ?? base.lowPriorityPaths),
|
|
95
|
-
];
|
|
96
|
-
const ignore = [...(override.ignore ?? base.ignore)];
|
|
97
|
-
const merged: Config = {
|
|
98
|
-
...base,
|
|
99
|
-
...override,
|
|
100
|
-
models,
|
|
101
|
-
ollama,
|
|
102
|
-
lowPriorityPaths,
|
|
103
|
-
ignore,
|
|
104
|
-
};
|
|
105
|
-
return merged;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Validate and normalize a parsed partial config, ignoring unknown keys. */
|
|
109
|
-
export function sanitizePartial(raw: unknown): PartialConfig {
|
|
110
|
-
if (raw === null || typeof raw !== "object") return {};
|
|
111
|
-
const obj = raw as Record<string, unknown>;
|
|
112
|
-
const out: PartialConfig = {};
|
|
113
|
-
|
|
114
|
-
const bool = (k: keyof Config) => {
|
|
115
|
-
if (typeof obj[k] === "boolean")
|
|
116
|
-
(out as Record<string, unknown>)[k] = obj[k];
|
|
117
|
-
};
|
|
118
|
-
bool("conventionalCommits");
|
|
119
|
-
bool("gitmoji");
|
|
120
|
-
bool("multiline");
|
|
121
|
-
bool("interactive");
|
|
122
|
-
bool("skipArmored");
|
|
123
|
-
bool("allowApiKey");
|
|
124
|
-
|
|
125
|
-
if (typeof obj.template === "string") out.template = obj.template;
|
|
126
|
-
else if (obj.template === null) out.template = null;
|
|
127
|
-
if (typeof obj.customPrompt === "string") out.customPrompt = obj.customPrompt;
|
|
128
|
-
else if (obj.customPrompt === null) out.customPrompt = null;
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
typeof obj.interactiveCount === "number" &&
|
|
132
|
-
Number.isFinite(obj.interactiveCount)
|
|
133
|
-
) {
|
|
134
|
-
out.interactiveCount = Math.max(1, Math.floor(obj.interactiveCount));
|
|
135
|
-
}
|
|
136
|
-
if (obj.interactiveTemperature === null) {
|
|
137
|
-
out.interactiveTemperature = null;
|
|
138
|
-
} else if (
|
|
139
|
-
typeof obj.interactiveTemperature === "number" &&
|
|
140
|
-
Number.isFinite(obj.interactiveTemperature)
|
|
141
|
-
) {
|
|
142
|
-
out.interactiveTemperature = Math.min(
|
|
143
|
-
2,
|
|
144
|
-
Math.max(0, obj.interactiveTemperature),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
if (typeof obj.spinner === "string" && isSpinnerName(obj.spinner)) {
|
|
148
|
-
out.spinner = obj.spinner;
|
|
149
|
-
}
|
|
150
|
-
if (typeof obj.maxChunkTokens === "number" && obj.maxChunkTokens > 0) {
|
|
151
|
-
out.maxChunkTokens = Math.floor(obj.maxChunkTokens);
|
|
152
|
-
}
|
|
153
|
-
if (typeof obj.charsPerToken === "number" && obj.charsPerToken > 0) {
|
|
154
|
-
out.charsPerToken = obj.charsPerToken;
|
|
155
|
-
}
|
|
156
|
-
// An explicit empty list is meaningful for either path option: it clears
|
|
157
|
-
// patterns inherited from a lower layer, so it is kept rather than
|
|
158
|
-
// treated as "unset".
|
|
159
|
-
if (Array.isArray(obj.lowPriorityPaths)) {
|
|
160
|
-
out.lowPriorityPaths = cleanPatternList(obj.lowPriorityPaths);
|
|
161
|
-
}
|
|
162
|
-
if (Array.isArray(obj.ignore)) {
|
|
163
|
-
out.ignore = cleanPatternList(obj.ignore);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (obj.models && typeof obj.models === "object") {
|
|
167
|
-
const m = obj.models as Record<string, unknown>;
|
|
168
|
-
const models: Partial<ModelConfig> = {};
|
|
169
|
-
// A blank model name is not an override, it is a mistake: leaving the
|
|
170
|
-
// key unset keeps the layer below, which is a working model.
|
|
171
|
-
if (typeof m.summary === "string" && m.summary.trim() !== "") {
|
|
172
|
-
models.summary = m.summary.trim();
|
|
173
|
-
}
|
|
174
|
-
if (typeof m.final === "string" && m.final.trim() !== "") {
|
|
175
|
-
models.final = m.final.trim();
|
|
176
|
-
}
|
|
177
|
-
if (Object.keys(models).length) out.models = models;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (obj.ollama && typeof obj.ollama === "object") {
|
|
181
|
-
const o = obj.ollama as Record<string, unknown>;
|
|
182
|
-
const ollama: Partial<OllamaConfig> = {};
|
|
183
|
-
if (typeof o.host === "string" && o.host.trim() !== "") {
|
|
184
|
-
ollama.host = o.host.trim();
|
|
185
|
-
}
|
|
186
|
-
if (typeof o.context === "number" && o.context > 0) {
|
|
187
|
-
ollama.context = Math.floor(o.context);
|
|
188
|
-
} else if (o.context === "auto") {
|
|
189
|
-
ollama.context = "auto";
|
|
190
|
-
}
|
|
191
|
-
if (typeof o.keepAlive === "string" || typeof o.keepAlive === "number") {
|
|
192
|
-
ollama.keepAlive = o.keepAlive;
|
|
193
|
-
} else if (o.keepAlive === null) {
|
|
194
|
-
ollama.keepAlive = null;
|
|
195
|
-
}
|
|
196
|
-
if (Object.keys(ollama).length) out.ollama = ollama;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Clean one raw path-pattern list: drop non-strings and blanks, trim the
|
|
204
|
-
* rest. Shared by `lowPriorityPaths` and `ignore`, which take the same
|
|
205
|
-
* pattern language (see `src/paths.ts`).
|
|
206
|
-
*/
|
|
207
|
-
function cleanPatternList(raw: unknown[]): string[] {
|
|
208
|
-
return raw
|
|
209
|
-
.filter((entry): entry is string => typeof entry === "string")
|
|
210
|
-
.map((entry) => entry.trim())
|
|
211
|
-
.filter((entry) => entry !== "");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function readJsonIfExists(path: string): Promise<unknown | undefined> {
|
|
215
|
-
const file = Bun.file(path);
|
|
216
|
-
if (!(await file.exists())) return undefined;
|
|
217
|
-
try {
|
|
218
|
-
return await file.json();
|
|
219
|
-
} catch (err) {
|
|
220
|
-
throw new ClaudeCommitError(
|
|
221
|
-
`Failed to parse config file ${path}: ${(err as Error).message}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Walk from `startDir` up to and including `rootDir`, returning the first config file found. */
|
|
227
|
-
async function findConfigFile(
|
|
228
|
-
startDir: string,
|
|
229
|
-
rootDir: string,
|
|
230
|
-
): Promise<string | undefined> {
|
|
231
|
-
let dir = resolve(startDir);
|
|
232
|
-
const stop = resolve(rootDir);
|
|
233
|
-
// Always terminates: we stop at `rootDir`, and `dirname` of the filesystem
|
|
234
|
-
// root returns itself (`parent === dir`), so even when `startDir` is not under
|
|
235
|
-
// `rootDir` the walk halts at the root regardless of directory depth.
|
|
236
|
-
for (;;) {
|
|
237
|
-
for (const name of CONFIG_FILENAMES) {
|
|
238
|
-
const candidate = join(dir, name);
|
|
239
|
-
if (await Bun.file(candidate).exists()) return candidate;
|
|
240
|
-
}
|
|
241
|
-
if (dir === stop) break;
|
|
242
|
-
const parent = dirname(dir);
|
|
243
|
-
if (parent === dir) break;
|
|
244
|
-
dir = parent;
|
|
245
|
-
}
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Load and merge the file-based configuration layers that sit below CLI flags,
|
|
251
|
-
* lowest first: the global user config, then `package.json`'s `claude-commit` key
|
|
252
|
-
* at the repo root, then the nearest project config file (searched from `cwd` up
|
|
253
|
-
* to `repoRoot`). An explicit `configPath` short-circuits the project-file
|
|
254
|
-
* discovery; the global and `package.json` layers still apply beneath it. `env`
|
|
255
|
-
* supplies `XDG_CONFIG_HOME` for locating the global config (defaults to
|
|
256
|
-
* `process.env`).
|
|
257
|
-
*/
|
|
258
|
-
export async function loadFileConfig(
|
|
259
|
-
cwd: string,
|
|
260
|
-
repoRoot: string,
|
|
261
|
-
configPath?: string,
|
|
262
|
-
env: Record<string, string | undefined> = process.env,
|
|
263
|
-
): Promise<PartialConfig> {
|
|
264
|
-
let result: PartialConfig = {};
|
|
265
|
-
|
|
266
|
-
// Global user config (lowest precedence): $XDG_CONFIG_HOME/claude-commit. Like
|
|
267
|
-
// a project config file, a malformed one throws (it is a file the user wrote
|
|
268
|
-
// deliberately), which readJsonIfExists handles.
|
|
269
|
-
const globalPath = await findGlobalConfigFile(env);
|
|
270
|
-
if (globalPath) {
|
|
271
|
-
result = mergePartial(
|
|
272
|
-
result,
|
|
273
|
-
sanitizePartial(await readJsonIfExists(globalPath)),
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// package.json#claude-commit at the repo root (above the global config, below
|
|
278
|
-
// project config files). A malformed package.json is not cco's concern to
|
|
279
|
-
// enforce - skip it rather than blocking the commit (the user may even be
|
|
280
|
-
// committing its fix).
|
|
281
|
-
let pkg: unknown;
|
|
282
|
-
try {
|
|
283
|
-
pkg = await readJsonIfExists(join(repoRoot, "package.json"));
|
|
284
|
-
} catch {
|
|
285
|
-
pkg = undefined;
|
|
286
|
-
}
|
|
287
|
-
if (pkg && typeof pkg === "object" && "claude-commit" in (pkg as object)) {
|
|
288
|
-
result = mergePartial(
|
|
289
|
-
result,
|
|
290
|
-
sanitizePartial((pkg as Record<string, unknown>)["claude-commit"]),
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const filePath = configPath
|
|
295
|
-
? resolve(cwd, configPath)
|
|
296
|
-
: await findConfigFile(cwd, repoRoot);
|
|
297
|
-
if (filePath) {
|
|
298
|
-
const raw = await readJsonIfExists(filePath);
|
|
299
|
-
if (raw === undefined && configPath) {
|
|
300
|
-
throw new ClaudeCommitError(`Config file not found: ${filePath}`);
|
|
301
|
-
}
|
|
302
|
-
result = mergePartial(result, sanitizePartial(raw));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return result;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Merge two partial configs: `models` and `ollama` are merged key by key;
|
|
310
|
-
* every other key, including both path lists, is taken whole from the
|
|
311
|
-
* override when present.
|
|
312
|
-
*/
|
|
313
|
-
export function mergePartial(
|
|
314
|
-
base: PartialConfig,
|
|
315
|
-
override: PartialConfig,
|
|
316
|
-
): PartialConfig {
|
|
317
|
-
const out: PartialConfig = { ...base, ...override };
|
|
318
|
-
if (base.models || override.models) {
|
|
319
|
-
out.models = { ...base.models, ...override.models };
|
|
320
|
-
}
|
|
321
|
-
if (base.ollama || override.ollama) {
|
|
322
|
-
out.ollama = { ...base.ollama, ...override.ollama };
|
|
323
|
-
}
|
|
324
|
-
const lowPriorityPaths = override.lowPriorityPaths ?? base.lowPriorityPaths;
|
|
325
|
-
if (lowPriorityPaths) out.lowPriorityPaths = [...lowPriorityPaths];
|
|
326
|
-
const ignore = override.ignore ?? base.ignore;
|
|
327
|
-
if (ignore) out.ignore = [...ignore];
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Produce a fully-resolved config from file config and CLI-flag overrides. */
|
|
332
|
-
export function resolveConfig(
|
|
333
|
-
fileConfig: PartialConfig,
|
|
334
|
-
flagConfig: PartialConfig,
|
|
335
|
-
): Config {
|
|
336
|
-
return mergeConfig(DEFAULT_CONFIG, mergePartial(fileConfig, flagConfig));
|
|
337
|
-
}
|