@kud/ai-conventional-commit-cli 3.1.1 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2WRUFO3O.js +689 -0
- package/dist/chunk-FYJNHXAR.js +700 -0
- package/dist/chunk-HOUMTU6H.js +699 -0
- package/dist/chunk-SNV4RWS4.js +696 -0
- package/dist/index.js +31 -15
- package/dist/reword-KUE3IVBE.js +212 -0
- package/dist/reword-MCQOCOZ2.js +212 -0
- package/dist/reword-T44WTP5I.js +212 -0
- package/dist/reword-UE5IP5V3.js +212 -0
- package/package.json +1 -1
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
// src/prompt.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
var matchesPattern = (filePath, pattern) => {
|
|
4
|
+
const regexPattern = pattern.replace(/\*\*/g, "\xA7DOUBLESTAR\xA7").replace(/\*/g, "[^/]*").replace(/§DOUBLESTAR§/g, ".*").replace(/\./g, "\\.").replace(/\?/g, ".");
|
|
5
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
6
|
+
return regex.test(filePath);
|
|
7
|
+
};
|
|
8
|
+
var MAX_DIFF_CHARS = 8e4;
|
|
9
|
+
var tag = (module) => chalk.dim("[ai-cc]") + chalk.cyan(`[${module}]`);
|
|
10
|
+
var kv = (k, v) => chalk.dim(k + "=") + chalk.yellow(String(v));
|
|
11
|
+
var dbg = (module, msg, pairs = {}) => {
|
|
12
|
+
if (process.env.AICC_DEBUG !== "true") return;
|
|
13
|
+
const kvStr = Object.entries(pairs).map(([k, v]) => kv(k, v)).join(" ");
|
|
14
|
+
console.error(tag(module), chalk.white(msg), kvStr || "");
|
|
15
|
+
};
|
|
16
|
+
var summarizeDiffForPrompt = (files, privacy, maxFileLines, skipFilePatterns) => {
|
|
17
|
+
const getTotalLines = (f) => {
|
|
18
|
+
return f.hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
19
|
+
};
|
|
20
|
+
const shouldSkipFile = (filePath) => {
|
|
21
|
+
return (skipFilePatterns ?? []).some((pattern) => matchesPattern(filePath, pattern));
|
|
22
|
+
};
|
|
23
|
+
const buildHigh = () => files.map((f) => {
|
|
24
|
+
const totalLines = getTotalLines(f);
|
|
25
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
26
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
27
|
+
const skipped = patternSkipped || sizeSkipped;
|
|
28
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
29
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}${skipped ? ` [${reason}, content skipped]` : ""}`;
|
|
30
|
+
}).join("\n");
|
|
31
|
+
if (privacy === "high") {
|
|
32
|
+
dbg("prompt", "privacy=high", { files: files.length });
|
|
33
|
+
return buildHigh();
|
|
34
|
+
}
|
|
35
|
+
const buildMedium = () => files.map((f) => {
|
|
36
|
+
const totalLines = getTotalLines(f);
|
|
37
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
38
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
39
|
+
if (patternSkipped || sizeSkipped) {
|
|
40
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
41
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
42
|
+
}
|
|
43
|
+
return `file: ${f.file}
|
|
44
|
+
` + f.hunks.map(
|
|
45
|
+
(h) => ` hunk ${h.hash} context:${h.functionContext || ""} +${h.added} -${h.removed}`
|
|
46
|
+
).join("\n");
|
|
47
|
+
}).join("\n");
|
|
48
|
+
if (privacy === "medium") {
|
|
49
|
+
const result = buildMedium();
|
|
50
|
+
dbg("prompt", "privacy=medium", { chars: result.length, budget: MAX_DIFF_CHARS });
|
|
51
|
+
if (result.length <= MAX_DIFF_CHARS) return result;
|
|
52
|
+
dbg("prompt", chalk.red("medium exceeds budget \u2192 degrading to high"));
|
|
53
|
+
return buildHigh();
|
|
54
|
+
}
|
|
55
|
+
const buildLow = () => files.map((f) => {
|
|
56
|
+
const totalLines = getTotalLines(f);
|
|
57
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
58
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
59
|
+
if (patternSkipped || sizeSkipped) {
|
|
60
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
61
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
62
|
+
}
|
|
63
|
+
return `file: ${f.file}
|
|
64
|
+
` + f.hunks.map(
|
|
65
|
+
(h) => `${h.header}
|
|
66
|
+
${h.lines.slice(0, 40).join("\n")}${h.lines.length > 40 ? "\n[truncated]" : ""}`
|
|
67
|
+
).join("\n");
|
|
68
|
+
}).join("\n");
|
|
69
|
+
const low = buildLow();
|
|
70
|
+
dbg("prompt", "privacy=low", { chars: low.length, budget: MAX_DIFF_CHARS });
|
|
71
|
+
if (low.length <= MAX_DIFF_CHARS) return low;
|
|
72
|
+
const medium = buildMedium();
|
|
73
|
+
dbg("prompt", chalk.yellow("low exceeds budget \u2192 trying medium"), { chars: medium.length });
|
|
74
|
+
if (medium.length <= MAX_DIFF_CHARS) return medium;
|
|
75
|
+
dbg("prompt", chalk.red("medium exceeds budget \u2192 degrading to high"));
|
|
76
|
+
return buildHigh();
|
|
77
|
+
};
|
|
78
|
+
var buildGenerationMessages = (opts) => {
|
|
79
|
+
const { files, style, config, mode, desiredCommits } = opts;
|
|
80
|
+
const diff = summarizeDiffForPrompt(
|
|
81
|
+
files,
|
|
82
|
+
config.privacy,
|
|
83
|
+
config.maxFileLines,
|
|
84
|
+
config.skipFilePatterns
|
|
85
|
+
);
|
|
86
|
+
const TYPE_MAP = {
|
|
87
|
+
feat: "A new feature or capability added for the user",
|
|
88
|
+
fix: "A bug fix resolving incorrect behavior",
|
|
89
|
+
chore: "Internal change with no user-facing impact",
|
|
90
|
+
docs: "Documentation-only changes",
|
|
91
|
+
refactor: "Code change that neither fixes a bug nor adds a feature",
|
|
92
|
+
test: "Adding or improving tests only",
|
|
93
|
+
ci: "Changes to CI configuration or scripts",
|
|
94
|
+
perf: "Performance improvement",
|
|
95
|
+
style: "Formatting or stylistic change (no logic)",
|
|
96
|
+
build: "Build system or dependency changes",
|
|
97
|
+
revert: "Revert a previous commit",
|
|
98
|
+
merge: "Merge branches (rare; only if truly a merge commit)",
|
|
99
|
+
security: "Security-related change or hardening",
|
|
100
|
+
release: "Version bump or release meta change"
|
|
101
|
+
};
|
|
102
|
+
const specLines = [];
|
|
103
|
+
specLines.push(
|
|
104
|
+
"Purpose: Generate high-quality Conventional Commit messages for the provided git diff."
|
|
105
|
+
);
|
|
106
|
+
specLines.push("Locale: en");
|
|
107
|
+
specLines.push(
|
|
108
|
+
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[], "files"?: string[] } ], "meta": { "splitRecommended": boolean } }'
|
|
109
|
+
);
|
|
110
|
+
specLines.push("Primary Output Field: commits[ ].title");
|
|
111
|
+
specLines.push("Title Format (REQUIRED): <type>(<scope>): <subject>");
|
|
112
|
+
specLines.push(
|
|
113
|
+
"Title Length Guidance: Aim for <=50 chars ideal; absolute max 72 (do not exceed)."
|
|
114
|
+
);
|
|
115
|
+
specLines.push("Types (JSON mapping follows on next line)");
|
|
116
|
+
specLines.push("TypeMap: " + JSON.stringify(TYPE_MAP));
|
|
117
|
+
specLines.push(
|
|
118
|
+
"Scope Rules: ALWAYS include a concise lowercase kebab-case scope (derive from dominant directory, package, or feature); never omit."
|
|
119
|
+
);
|
|
120
|
+
specLines.push(
|
|
121
|
+
"Subject Rules: imperative mood, present tense, no leading capital unless proper noun, no trailing period."
|
|
122
|
+
);
|
|
123
|
+
specLines.push(
|
|
124
|
+
"Length Rule: Keep titles concise; prefer 50 or fewer chars; MUST be <=72 including type/scope."
|
|
125
|
+
);
|
|
126
|
+
specLines.push(
|
|
127
|
+
"Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE the type only if confidently adds clarity; do not invent or stack; omit if unsure." : "Disallow all emojis and gitmoji codes; output must start directly with the type.")
|
|
128
|
+
);
|
|
129
|
+
specLines.push(
|
|
130
|
+
"Forbidden: breaking changes notation, exclamation mark after type unless truly semver-major (avoid unless diff clearly indicates)."
|
|
131
|
+
);
|
|
132
|
+
specLines.push("Fallback Type: use chore when no other type clearly fits.");
|
|
133
|
+
specLines.push("Consistency: prefer existing top prefixes: " + style.topPrefixes.join(", "));
|
|
134
|
+
specLines.push("Provide score (0-100) measuring clarity & specificity (higher is better).");
|
|
135
|
+
specLines.push(
|
|
136
|
+
"Provide reasons array citing concrete diff elements: filenames, functions, tests, metrics."
|
|
137
|
+
);
|
|
138
|
+
specLines.push(
|
|
139
|
+
'When mode is split, WHERE POSSIBLE add a "files" array per commit listing the most relevant changed file paths (1-6, minimize overlap across commits).'
|
|
140
|
+
);
|
|
141
|
+
specLines.push("Return ONLY the JSON object. No surrounding text or markdown.");
|
|
142
|
+
specLines.push("Do not add fields not listed in schema.");
|
|
143
|
+
specLines.push("Never fabricate content not present or implied by the diff.");
|
|
144
|
+
specLines.push(
|
|
145
|
+
"If mode is split and multiple logical changes exist, set meta.splitRecommended=true."
|
|
146
|
+
);
|
|
147
|
+
const messages = [
|
|
148
|
+
{
|
|
149
|
+
role: "system",
|
|
150
|
+
content: specLines.join("\n")
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
role: "user",
|
|
154
|
+
content: `Mode: ${mode}
|
|
155
|
+
RequestedCommitCount: ${desiredCommits || (mode === "split" ? "2-6" : 1)}
|
|
156
|
+
StyleFingerprint: ${JSON.stringify(style)}
|
|
157
|
+
Diff:
|
|
158
|
+
${diff}
|
|
159
|
+
Generate commit candidates now.`
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
dbg("prompt", "messages built", { systemChars: messages[0].content.length, userChars: messages[1].content.length, totalChars: messages[0].content.length + messages[1].content.length });
|
|
163
|
+
return messages;
|
|
164
|
+
};
|
|
165
|
+
var buildRefineMessages = (opts) => {
|
|
166
|
+
const { originalPlan, index, instructions, config } = opts;
|
|
167
|
+
const target = originalPlan.commits[index];
|
|
168
|
+
const spec = [];
|
|
169
|
+
spec.push("Purpose: Refine a single Conventional Commit message while preserving intent.");
|
|
170
|
+
spec.push("Locale: en");
|
|
171
|
+
spec.push("Input: one existing commit JSON object.");
|
|
172
|
+
spec.push(
|
|
173
|
+
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ] }'
|
|
174
|
+
);
|
|
175
|
+
spec.push("Title Format (REQUIRED): <type>(<scope>): <subject> (<=72 chars)");
|
|
176
|
+
spec.push("Subject: imperative, present tense, no trailing period.");
|
|
177
|
+
spec.push(
|
|
178
|
+
"Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE type if it adds clarity; omit if unsure." : "Disallow all emojis; start directly with the type.")
|
|
179
|
+
);
|
|
180
|
+
spec.push(
|
|
181
|
+
"Preserve semantic meaning; ensure a scope is present (infer one if missing); only improve clarity, brevity, conformity."
|
|
182
|
+
);
|
|
183
|
+
spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
|
|
184
|
+
spec.push("Return ONLY JSON (commits array length=1).");
|
|
185
|
+
return [
|
|
186
|
+
{ role: "system", content: spec.join("\n") },
|
|
187
|
+
{
|
|
188
|
+
role: "user",
|
|
189
|
+
content: `Current commit object:
|
|
190
|
+
${JSON.stringify(target, null, 2)}
|
|
191
|
+
Instructions:
|
|
192
|
+
${instructions.join("\n") || "None"}
|
|
193
|
+
Refine now.`
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/model/provider.ts
|
|
199
|
+
import { z } from "zod";
|
|
200
|
+
import { createServer } from "net";
|
|
201
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
202
|
+
import { join } from "path";
|
|
203
|
+
import { tmpdir } from "os";
|
|
204
|
+
import { createOpencode } from "@opencode-ai/sdk/v2";
|
|
205
|
+
import chalk2 from "chalk";
|
|
206
|
+
var pTag = chalk2.dim("[ai-cc]") + chalk2.cyan("[provider]");
|
|
207
|
+
var pdbg = (msg, pairs = {}) => {
|
|
208
|
+
const kvStr = Object.entries(pairs).map(([k, v]) => chalk2.dim(k + "=") + chalk2.yellow(String(v))).join(" ");
|
|
209
|
+
console.error(pTag, chalk2.white(msg), kvStr || "");
|
|
210
|
+
};
|
|
211
|
+
function findFreePort() {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const server = createServer();
|
|
214
|
+
server.on("error", reject);
|
|
215
|
+
server.listen(0, "127.0.0.1", () => {
|
|
216
|
+
const port = server.address().port;
|
|
217
|
+
server.close(() => resolve(port));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
var OpenCodeProvider = class {
|
|
222
|
+
constructor(model = "github-copilot/gpt-4.1") {
|
|
223
|
+
this.model = model;
|
|
224
|
+
this.timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
|
|
225
|
+
this.debug = process.env.AICC_DEBUG === "true";
|
|
226
|
+
setTimeout(() => this.ac.abort(), this.timeoutMs);
|
|
227
|
+
this.exitHandler = () => {
|
|
228
|
+
void this._closeServer();
|
|
229
|
+
};
|
|
230
|
+
process.once("exit", this.exitHandler);
|
|
231
|
+
process.once("SIGINT", this.exitHandler);
|
|
232
|
+
process.once("SIGTERM", this.exitHandler);
|
|
233
|
+
}
|
|
234
|
+
warmPromise = null;
|
|
235
|
+
ac = new AbortController();
|
|
236
|
+
timeoutMs;
|
|
237
|
+
debug;
|
|
238
|
+
exitHandler;
|
|
239
|
+
async _closeServer() {
|
|
240
|
+
this.ac.abort();
|
|
241
|
+
if (!this.warmPromise) return;
|
|
242
|
+
try {
|
|
243
|
+
const ctx = await this.warmPromise;
|
|
244
|
+
ctx.server?.close();
|
|
245
|
+
} catch {
|
|
246
|
+
} finally {
|
|
247
|
+
process.off("exit", this.exitHandler);
|
|
248
|
+
process.off("SIGINT", this.exitHandler);
|
|
249
|
+
process.off("SIGTERM", this.exitHandler);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async close() {
|
|
253
|
+
return this._closeServer();
|
|
254
|
+
}
|
|
255
|
+
name() {
|
|
256
|
+
return "opencode";
|
|
257
|
+
}
|
|
258
|
+
warmup() {
|
|
259
|
+
if (!this.warmPromise) {
|
|
260
|
+
this.warmPromise = this._startServer();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async _startServer() {
|
|
264
|
+
if (this.debug) pdbg("starting opencode server");
|
|
265
|
+
const isolatedDir = join(tmpdir(), `aicc-${process.pid}`, "opencode");
|
|
266
|
+
mkdirSync(isolatedDir, { recursive: true });
|
|
267
|
+
writeFileSync(join(isolatedDir, "config.json"), '{"mcp":{}}');
|
|
268
|
+
const originalXDG = process.env.XDG_CONFIG_HOME;
|
|
269
|
+
process.env.XDG_CONFIG_HOME = join(tmpdir(), `aicc-${process.pid}`);
|
|
270
|
+
let opencode;
|
|
271
|
+
try {
|
|
272
|
+
const port = await findFreePort();
|
|
273
|
+
opencode = await createOpencode({ signal: this.ac.signal, port });
|
|
274
|
+
} finally {
|
|
275
|
+
if (originalXDG === void 0) delete process.env.XDG_CONFIG_HOME;
|
|
276
|
+
else process.env.XDG_CONFIG_HOME = originalXDG;
|
|
277
|
+
}
|
|
278
|
+
const { server, client } = opencode;
|
|
279
|
+
if (this.debug) {
|
|
280
|
+
const mcpStatusResult = await client.mcp.status();
|
|
281
|
+
pdbg("mcp status", { status: JSON.stringify(mcpStatusResult.data) });
|
|
282
|
+
}
|
|
283
|
+
const sessionResult = await client.session.create({ title: "aicc" });
|
|
284
|
+
if (!sessionResult.data) {
|
|
285
|
+
const errMsg = sessionResult.error?.message ?? JSON.stringify(sessionResult.error) ?? "unknown";
|
|
286
|
+
throw new Error(`Failed to create opencode session: ${errMsg}`);
|
|
287
|
+
}
|
|
288
|
+
if (this.debug) pdbg("session created", { id: sessionResult.data.id });
|
|
289
|
+
return { client, server, sessionID: sessionResult.data.id };
|
|
290
|
+
}
|
|
291
|
+
async chat(messages, _opts) {
|
|
292
|
+
const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
|
|
293
|
+
if (mockMode) {
|
|
294
|
+
if (this.debug) pdbg("mock mode \u2014 returning deterministic response");
|
|
295
|
+
return JSON.stringify({
|
|
296
|
+
commits: [
|
|
297
|
+
{
|
|
298
|
+
title: "chore: mock commit from provider",
|
|
299
|
+
body: "",
|
|
300
|
+
score: 80,
|
|
301
|
+
reasons: ["mock mode"]
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
meta: { splitRecommended: false }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
308
|
+
const fullPrompt = `Generate high-quality commit message candidates based on the staged git diff.
|
|
309
|
+
|
|
310
|
+
Context:
|
|
311
|
+
${userAggregate}`;
|
|
312
|
+
const slashIdx = this.model.indexOf("/");
|
|
313
|
+
const providerID = slashIdx !== -1 ? this.model.slice(0, slashIdx) : this.model;
|
|
314
|
+
const modelID = slashIdx !== -1 ? this.model.slice(slashIdx + 1) : this.model;
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
let server;
|
|
317
|
+
try {
|
|
318
|
+
const ctx = await (this.warmPromise ?? this._startServer());
|
|
319
|
+
server = ctx.server;
|
|
320
|
+
if (this.debug) {
|
|
321
|
+
pdbg("sending prompt", { model: this.model, promptChars: fullPrompt.length });
|
|
322
|
+
}
|
|
323
|
+
const result = await ctx.client.session.prompt({
|
|
324
|
+
sessionID: ctx.sessionID,
|
|
325
|
+
model: { providerID, modelID },
|
|
326
|
+
format: {
|
|
327
|
+
type: "json_schema",
|
|
328
|
+
schema: COMMIT_PLAN_JSON_SCHEMA
|
|
329
|
+
},
|
|
330
|
+
parts: [{ type: "text", text: fullPrompt }]
|
|
331
|
+
});
|
|
332
|
+
if (this.debug) {
|
|
333
|
+
const elapsed = Date.now() - start;
|
|
334
|
+
pdbg("response received", { model: this.model, elapsedMs: elapsed, promptChars: fullPrompt.length });
|
|
335
|
+
pdbg("result.data", { json: JSON.stringify(result.data, null, 2) });
|
|
336
|
+
}
|
|
337
|
+
const structured = result.data?.info?.structured;
|
|
338
|
+
if (structured == null) {
|
|
339
|
+
const err = result.data?.info?.error;
|
|
340
|
+
throw new Error(
|
|
341
|
+
err ? `Model error: ${JSON.stringify(err)}` : "No structured output in response"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return JSON.stringify(structured);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
if (this.ac.signal.aborted) {
|
|
347
|
+
throw new Error(`Model call timed out after ${this.timeoutMs}ms`);
|
|
348
|
+
}
|
|
349
|
+
if (this.debug) pdbg(chalk2.red("call failed"), { error: e.message });
|
|
350
|
+
throw new Error(e.message || "opencode SDK call failed");
|
|
351
|
+
} finally {
|
|
352
|
+
server?.close();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var CommitSchema = z.object({
|
|
357
|
+
title: z.string().min(5).max(150),
|
|
358
|
+
body: z.string().optional().default(""),
|
|
359
|
+
score: z.number().min(0).max(100),
|
|
360
|
+
reasons: z.array(z.string()).optional().default([]),
|
|
361
|
+
files: z.array(z.string()).optional().default([])
|
|
362
|
+
});
|
|
363
|
+
var PlanSchema = z.object({
|
|
364
|
+
commits: z.array(CommitSchema).min(1),
|
|
365
|
+
meta: z.object({
|
|
366
|
+
splitRecommended: z.boolean().optional()
|
|
367
|
+
}).optional()
|
|
368
|
+
});
|
|
369
|
+
var COMMIT_PLAN_JSON_SCHEMA = {
|
|
370
|
+
type: "object",
|
|
371
|
+
required: ["commits"],
|
|
372
|
+
properties: {
|
|
373
|
+
commits: {
|
|
374
|
+
type: "array",
|
|
375
|
+
minItems: 1,
|
|
376
|
+
items: {
|
|
377
|
+
type: "object",
|
|
378
|
+
required: ["title", "score"],
|
|
379
|
+
properties: {
|
|
380
|
+
title: { type: "string" },
|
|
381
|
+
body: { type: "string" },
|
|
382
|
+
score: { type: "number", minimum: 0, maximum: 100 },
|
|
383
|
+
reasons: { type: "array", items: { type: "string" } },
|
|
384
|
+
files: { type: "array", items: { type: "string" } }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
meta: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
splitRecommended: { type: "boolean" }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
var extractJSON = (raw) => {
|
|
397
|
+
const trimmed = raw.trim();
|
|
398
|
+
let jsonText = null;
|
|
399
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
400
|
+
jsonText = trimmed;
|
|
401
|
+
} else {
|
|
402
|
+
const match = raw.match(/\{[\s\S]*\}$/m);
|
|
403
|
+
if (match) jsonText = match[0];
|
|
404
|
+
}
|
|
405
|
+
if (!jsonText) throw new Error("No JSON object detected.");
|
|
406
|
+
let parsed;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(jsonText);
|
|
409
|
+
} catch {
|
|
410
|
+
throw new Error("Invalid JSON parse");
|
|
411
|
+
}
|
|
412
|
+
return PlanSchema.parse(parsed);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/guardrails.ts
|
|
416
|
+
var SECRET_PATTERNS = [
|
|
417
|
+
/AWS_[A-Z0-9_]+/i,
|
|
418
|
+
/BEGIN RSA PRIVATE KEY/,
|
|
419
|
+
/-----BEGIN PRIVATE KEY-----/,
|
|
420
|
+
/ssh-rsa AAAA/
|
|
421
|
+
];
|
|
422
|
+
var CONVENTIONAL_RE = /^(?:([\p{Emoji}\p{So}\p{Sk}]+)\s+(feat|fix|chore|docs|refactor|test|ci|perf|style|build|revert|merge|security|release)(\(.+\))?:\s|([\p{Emoji}\p{So}\p{Sk}]+):\s.*|([\p{Emoji}\p{So}\p{Sk}]+):\s*$|(feat|fix|chore|docs|refactor|test|ci|perf|style|build|revert|merge|security|release)(\(.+\))?:\s)/u;
|
|
423
|
+
var sanitizeTitle = (title, allowEmoji) => {
|
|
424
|
+
let t = title.trim();
|
|
425
|
+
if (allowEmoji) {
|
|
426
|
+
const multi = t.match(/^((?:[\p{Emoji}\p{So}\p{Sk}]+)[\p{Emoji}\p{So}\p{Sk}\s]*)+/u);
|
|
427
|
+
if (multi) {
|
|
428
|
+
const first = Array.from(multi[0].trim())[0];
|
|
429
|
+
t = first + " " + t.slice(multi[0].length).trimStart();
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
t = t.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trimStart();
|
|
433
|
+
}
|
|
434
|
+
return t;
|
|
435
|
+
};
|
|
436
|
+
var normalizeConventionalTitle = (title) => {
|
|
437
|
+
let original = title.trim();
|
|
438
|
+
let leadingEmoji = "";
|
|
439
|
+
const emojiCluster = original.match(/^[\p{Emoji}\p{So}\p{Sk}]+/u);
|
|
440
|
+
if (emojiCluster) {
|
|
441
|
+
leadingEmoji = Array.from(emojiCluster[0])[0];
|
|
442
|
+
}
|
|
443
|
+
let t = original.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trim();
|
|
444
|
+
const m = t.match(/^(\w+)(\(.+\))?:\s+(.*)$/);
|
|
445
|
+
let result;
|
|
446
|
+
if (m) {
|
|
447
|
+
const type = m[1].toLowerCase();
|
|
448
|
+
const scope = m[2] || "";
|
|
449
|
+
let subject = m[3].trim();
|
|
450
|
+
subject = subject.replace(/\.$/, "");
|
|
451
|
+
subject = subject.charAt(0).toLowerCase() + subject.slice(1);
|
|
452
|
+
result = `${type}${scope}: ${subject}`;
|
|
453
|
+
} else if (!/^\w+\(.+\)?: /.test(t)) {
|
|
454
|
+
t = t.replace(/\.$/, "");
|
|
455
|
+
t = t.charAt(0).toLowerCase() + t.slice(1);
|
|
456
|
+
result = `chore: ${t}`;
|
|
457
|
+
} else {
|
|
458
|
+
result = t;
|
|
459
|
+
}
|
|
460
|
+
if (leadingEmoji) {
|
|
461
|
+
result = `${leadingEmoji} ${result}`;
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
};
|
|
465
|
+
var checkCandidate = (candidate) => {
|
|
466
|
+
const errs = [];
|
|
467
|
+
if (!CONVENTIONAL_RE.test(candidate.title)) {
|
|
468
|
+
errs.push("Not a valid conventional commit title.");
|
|
469
|
+
}
|
|
470
|
+
if (/^[A-Z]/.test(candidate.title)) {
|
|
471
|
+
}
|
|
472
|
+
const body = candidate.body || "";
|
|
473
|
+
for (const pat of SECRET_PATTERNS) {
|
|
474
|
+
if (pat.test(body)) {
|
|
475
|
+
errs.push("Potential secret detected.");
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return errs;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/title-format.ts
|
|
483
|
+
var EMOJI_MAP = {
|
|
484
|
+
feat: "\u2728",
|
|
485
|
+
fix: "\u{1F41B}",
|
|
486
|
+
chore: "\u{1F9F9}",
|
|
487
|
+
docs: "\u{1F4DD}",
|
|
488
|
+
refactor: "\u267B\uFE0F",
|
|
489
|
+
test: "\u2705",
|
|
490
|
+
ci: "\u{1F916}",
|
|
491
|
+
perf: "\u26A1\uFE0F",
|
|
492
|
+
style: "\u{1F3A8}",
|
|
493
|
+
build: "\u{1F3D7}\uFE0F",
|
|
494
|
+
revert: "\u23EA",
|
|
495
|
+
merge: "\u{1F500}",
|
|
496
|
+
security: "\u{1F512}",
|
|
497
|
+
release: "\u{1F3F7}\uFE0F"
|
|
498
|
+
};
|
|
499
|
+
var EMOJI_TYPE_RE = /^([\p{Emoji}\p{So}\p{Sk}])\s+(\w+)(\(.+\))?:\s+(.*)$/u;
|
|
500
|
+
var TYPE_RE = /^(\w+)(\(.+\))?:\s+(.*)$/;
|
|
501
|
+
var formatCommitTitle = (raw, opts) => {
|
|
502
|
+
const { allowGitmoji, mode = "standard" } = opts;
|
|
503
|
+
let norm = normalizeConventionalTitle(sanitizeTitle(raw, allowGitmoji));
|
|
504
|
+
if (!allowGitmoji || mode !== "gitmoji" && mode !== "gitmoji-pure") {
|
|
505
|
+
return norm;
|
|
506
|
+
}
|
|
507
|
+
if (mode === "gitmoji-pure") {
|
|
508
|
+
let m2 = norm.match(EMOJI_TYPE_RE);
|
|
509
|
+
if (m2) {
|
|
510
|
+
const emoji = m2[1];
|
|
511
|
+
const subject = m2[4];
|
|
512
|
+
norm = `${emoji}: ${subject}`;
|
|
513
|
+
} else if (m2 = norm.match(TYPE_RE)) {
|
|
514
|
+
const type = m2[1];
|
|
515
|
+
const subject = m2[3];
|
|
516
|
+
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
517
|
+
norm = `${em}: ${subject}`;
|
|
518
|
+
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+:/u.test(norm)) {
|
|
519
|
+
norm = `\u{1F527}: ${norm}`;
|
|
520
|
+
}
|
|
521
|
+
return norm;
|
|
522
|
+
}
|
|
523
|
+
let m = norm.match(EMOJI_TYPE_RE);
|
|
524
|
+
if (m) {
|
|
525
|
+
return norm;
|
|
526
|
+
}
|
|
527
|
+
if (m = norm.match(TYPE_RE)) {
|
|
528
|
+
const type = m[1];
|
|
529
|
+
const scope = m[2] || "";
|
|
530
|
+
const subject = m[3];
|
|
531
|
+
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
532
|
+
norm = `${em} ${type}${scope}: ${subject}`;
|
|
533
|
+
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+\s+\w+.*:/u.test(norm)) {
|
|
534
|
+
norm = `\u{1F527} chore: ${norm}`;
|
|
535
|
+
}
|
|
536
|
+
return norm;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/workflow/ui.ts
|
|
540
|
+
import chalk3 from "chalk";
|
|
541
|
+
function animateHeaderBase(text = "ai-conventional-commit", modelSegment) {
|
|
542
|
+
const mainText = text;
|
|
543
|
+
const modelSeg = modelSegment ? ` (using ${modelSegment})` : "";
|
|
544
|
+
if (!process.stdout.isTTY || process.env.AICC_NO_ANIMATION) {
|
|
545
|
+
if (modelSeg) console.log("\n\u250C " + chalk3.bold(mainText) + chalk3.dim(modelSeg));
|
|
546
|
+
else console.log("\n\u250C " + chalk3.bold(mainText));
|
|
547
|
+
return Promise.resolve();
|
|
548
|
+
}
|
|
549
|
+
const palette = [
|
|
550
|
+
"#3a0d6d",
|
|
551
|
+
"#5a1ea3",
|
|
552
|
+
"#7a32d6",
|
|
553
|
+
"#9a4dff",
|
|
554
|
+
"#b267ff",
|
|
555
|
+
"#c37dff",
|
|
556
|
+
"#b267ff",
|
|
557
|
+
"#9a4dff",
|
|
558
|
+
"#7a32d6",
|
|
559
|
+
"#5a1ea3"
|
|
560
|
+
];
|
|
561
|
+
process.stdout.write("\n");
|
|
562
|
+
return palette.reduce(async (p, color) => {
|
|
563
|
+
await p;
|
|
564
|
+
const frame = chalk3.bold.hex(color)(mainText);
|
|
565
|
+
if (modelSeg) process.stdout.write("\r\u250C " + frame + chalk3.dim(modelSeg));
|
|
566
|
+
else process.stdout.write("\r\u250C " + frame);
|
|
567
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
568
|
+
}, Promise.resolve()).then(() => process.stdout.write("\n"));
|
|
569
|
+
}
|
|
570
|
+
function borderLine(content) {
|
|
571
|
+
if (!content) console.log("\u2502");
|
|
572
|
+
else console.log("\u2502 " + content);
|
|
573
|
+
}
|
|
574
|
+
function sectionTitle(label) {
|
|
575
|
+
console.log("\u2299 " + chalk3.bold(label));
|
|
576
|
+
}
|
|
577
|
+
function abortMessage() {
|
|
578
|
+
console.log("\u2514 \u{1F645}\u200D\u2640\uFE0F No commit created.");
|
|
579
|
+
console.log();
|
|
580
|
+
}
|
|
581
|
+
function finalSuccess(opts) {
|
|
582
|
+
const elapsedMs = Date.now() - opts.startedAt;
|
|
583
|
+
const seconds = elapsedMs / 1e3;
|
|
584
|
+
const dur = seconds >= 0.1 ? seconds.toFixed(1) + "s" : elapsedMs + "ms";
|
|
585
|
+
const plural = opts.count !== 1;
|
|
586
|
+
if (plural) console.log(`\u2514 \u2728 ${opts.count} commits created in ${dur}.`);
|
|
587
|
+
else console.log(`\u2514 \u2728 commit created in ${dur}.`);
|
|
588
|
+
console.log();
|
|
589
|
+
}
|
|
590
|
+
function createPhasedSpinner(oraLib) {
|
|
591
|
+
const useAnim = process.stdout.isTTY && !process.env.AICC_NO_ANIMATION && !process.env.AICC_NO_SPINNER_ANIM;
|
|
592
|
+
const palette = [
|
|
593
|
+
"#3a0d6d",
|
|
594
|
+
"#5a1ea3",
|
|
595
|
+
"#7a32d6",
|
|
596
|
+
"#9a4dff",
|
|
597
|
+
"#b267ff",
|
|
598
|
+
"#c37dff",
|
|
599
|
+
"#b267ff",
|
|
600
|
+
"#9a4dff",
|
|
601
|
+
"#7a32d6",
|
|
602
|
+
"#5a1ea3"
|
|
603
|
+
];
|
|
604
|
+
let label = "Starting";
|
|
605
|
+
let i = 0;
|
|
606
|
+
const spinner = oraLib({ text: chalk3.bold(label), spinner: "dots" }).start();
|
|
607
|
+
let interval = null;
|
|
608
|
+
function frame() {
|
|
609
|
+
if (!useAnim) return;
|
|
610
|
+
spinner.text = chalk3.bold.hex(palette[i])(label);
|
|
611
|
+
i = (i + 1) % palette.length;
|
|
612
|
+
}
|
|
613
|
+
if (useAnim) {
|
|
614
|
+
frame();
|
|
615
|
+
interval = setInterval(frame, 80);
|
|
616
|
+
}
|
|
617
|
+
function setLabel(next) {
|
|
618
|
+
label = next;
|
|
619
|
+
if (useAnim) {
|
|
620
|
+
i = 0;
|
|
621
|
+
frame();
|
|
622
|
+
} else {
|
|
623
|
+
spinner.text = chalk3.bold(label);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function stopAnim() {
|
|
627
|
+
if (interval) {
|
|
628
|
+
clearInterval(interval);
|
|
629
|
+
interval = null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
spinner,
|
|
634
|
+
async step(l, fn) {
|
|
635
|
+
setLabel(l);
|
|
636
|
+
try {
|
|
637
|
+
return await fn();
|
|
638
|
+
} catch (e) {
|
|
639
|
+
stopAnim();
|
|
640
|
+
const msg = `${l} failed: ${e?.message || e}`.replace(/^\s+/, "");
|
|
641
|
+
spinner.fail(msg);
|
|
642
|
+
throw e;
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
phase(l) {
|
|
646
|
+
setLabel(l);
|
|
647
|
+
},
|
|
648
|
+
stop() {
|
|
649
|
+
stopAnim();
|
|
650
|
+
spinner.stop();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function renderCommitBlock(opts) {
|
|
655
|
+
const dim = (s) => chalk3.dim(s);
|
|
656
|
+
const white = (s) => chalk3.white(s);
|
|
657
|
+
const msgColor = opts.messageLabelColor || dim;
|
|
658
|
+
const descColor = opts.descriptionLabelColor || dim;
|
|
659
|
+
const titleColor = opts.titleColor || white;
|
|
660
|
+
const bodyFirst = opts.bodyFirstLineColor || white;
|
|
661
|
+
const bodyRest = opts.bodyLineColor || white;
|
|
662
|
+
if (opts.fancy) {
|
|
663
|
+
const heading = opts.heading ? chalk3.hex("#9a4dff").bold(opts.heading) : void 0;
|
|
664
|
+
if (heading) borderLine(heading);
|
|
665
|
+
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
666
|
+
} else {
|
|
667
|
+
if (opts.heading) borderLine(chalk3.bold(opts.heading));
|
|
668
|
+
if (!opts.hideMessageLabel)
|
|
669
|
+
borderLine(msgColor("Message:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
670
|
+
else
|
|
671
|
+
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
672
|
+
}
|
|
673
|
+
borderLine();
|
|
674
|
+
if (opts.body) {
|
|
675
|
+
const lines = opts.body.split("\n");
|
|
676
|
+
lines.forEach((line, i) => {
|
|
677
|
+
if (line.trim().length === 0) borderLine();
|
|
678
|
+
else if (i === 0) {
|
|
679
|
+
borderLine(descColor("Description:"));
|
|
680
|
+
borderLine(bodyFirst(line));
|
|
681
|
+
} else borderLine(bodyRest(line));
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export {
|
|
687
|
+
buildGenerationMessages,
|
|
688
|
+
buildRefineMessages,
|
|
689
|
+
OpenCodeProvider,
|
|
690
|
+
extractJSON,
|
|
691
|
+
checkCandidate,
|
|
692
|
+
formatCommitTitle,
|
|
693
|
+
animateHeaderBase,
|
|
694
|
+
borderLine,
|
|
695
|
+
sectionTitle,
|
|
696
|
+
abortMessage,
|
|
697
|
+
finalSuccess,
|
|
698
|
+
createPhasedSpinner,
|
|
699
|
+
renderCommitBlock
|
|
700
|
+
};
|