@kud/ai-conventional-commit-cli 0.12.12 → 0.13.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/README.md +25 -2
- package/dist/chunk-HJR5M6U7.js +120 -0
- package/dist/chunk-WW3N76NL.js +586 -0
- package/dist/config-C3S4LWLD.js +12 -0
- package/dist/index.js +8 -8
- package/dist/reword-Q7MES34W.js +212 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,10 +221,31 @@ Resolves via cosmiconfig (JSON/YAML/etc). Example:
|
|
|
221
221
|
"style": "gitmoji",
|
|
222
222
|
"styleSamples": 120,
|
|
223
223
|
"plugins": ["./src/sample-plugin/example-plugin.ts"],
|
|
224
|
-
"maxTokens": 512
|
|
224
|
+
"maxTokens": 512,
|
|
225
|
+
"maxFileLines": 1000,
|
|
226
|
+
"skipFilePatterns": [
|
|
227
|
+
"**/package-lock.json",
|
|
228
|
+
"**/yarn.lock",
|
|
229
|
+
"**/*.d.ts",
|
|
230
|
+
"**/dist/**"
|
|
231
|
+
]
|
|
225
232
|
}
|
|
226
233
|
```
|
|
227
234
|
|
|
235
|
+
### Configuration Options
|
|
236
|
+
|
|
237
|
+
- **`maxFileLines`** (default: 1000): Skip file content in AI prompt if diff line count exceeds this threshold (per file). Files are still committed - the AI just sees the filename and stats instead of full content. Helps avoid token overflow from extremely large files.
|
|
238
|
+
|
|
239
|
+
- **`skipFilePatterns`** (default: see below): Glob patterns for files whose content should be skipped in the AI prompt but still committed. Useful for generated files, lock files, and build artifacts.
|
|
240
|
+
|
|
241
|
+
**Default patterns:**
|
|
242
|
+
- Lock files: `**/package-lock.json`, `**/yarn.lock`, `**/pnpm-lock.yaml`, `**/bun.lockb`, `**/composer.lock`, `**/Gemfile.lock`, `**/Cargo.lock`, `**/poetry.lock`
|
|
243
|
+
- TypeScript declarations: `**/*.d.ts`
|
|
244
|
+
- Build output: `**/dist/**`, `**/build/**`, `**/.next/**`, `**/out/**`, `**/coverage/**`
|
|
245
|
+
- Minified files: `**/*.min.js`, `**/*.min.css`, `**/*.map`
|
|
246
|
+
|
|
247
|
+
To override, provide your own array in config. To disable entirely, set to `[]`.
|
|
248
|
+
|
|
228
249
|
Environment overrides (prefix `AICC_`):
|
|
229
250
|
|
|
230
251
|
### Configuration Precedence
|
|
@@ -253,7 +274,9 @@ ai-conventional-commit models --interactive --save # pick + persist globally
|
|
|
253
274
|
ai-conventional-commit models --current # show active model + source
|
|
254
275
|
```
|
|
255
276
|
|
|
256
|
-
`MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `VERBOSE`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
|
|
277
|
+
`MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `MAX_FILE_LINES`, `VERBOSE`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
|
|
278
|
+
|
|
279
|
+
**Note:** `skipFilePatterns` cannot be set via environment variable - use config file or accept defaults.
|
|
257
280
|
|
|
258
281
|
## Refinement Workflow
|
|
259
282
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
3
|
+
import { resolve, dirname, join } from "path";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
var DEFAULTS = {
|
|
7
|
+
model: process.env.AICC_MODEL || "github-copilot/gpt-4.1",
|
|
8
|
+
privacy: process.env.AICC_PRIVACY || "low",
|
|
9
|
+
style: process.env.AICC_STYLE || "standard",
|
|
10
|
+
styleSamples: parseInt(process.env.AICC_STYLE_SAMPLES || "120", 10),
|
|
11
|
+
maxTokens: parseInt(process.env.AICC_MAX_TOKENS || "512", 10),
|
|
12
|
+
maxFileLines: parseInt(process.env.AICC_MAX_FILE_LINES || "1000", 10),
|
|
13
|
+
skipFilePatterns: [
|
|
14
|
+
"**/package-lock.json",
|
|
15
|
+
"**/yarn.lock",
|
|
16
|
+
"**/pnpm-lock.yaml",
|
|
17
|
+
"**/bun.lockb",
|
|
18
|
+
"**/composer.lock",
|
|
19
|
+
"**/Gemfile.lock",
|
|
20
|
+
"**/Cargo.lock",
|
|
21
|
+
"**/poetry.lock",
|
|
22
|
+
"**/*.d.ts",
|
|
23
|
+
"**/dist/**",
|
|
24
|
+
"**/build/**",
|
|
25
|
+
"**/.next/**",
|
|
26
|
+
"**/out/**",
|
|
27
|
+
"**/coverage/**",
|
|
28
|
+
"**/*.min.js",
|
|
29
|
+
"**/*.min.css",
|
|
30
|
+
"**/*.map"
|
|
31
|
+
],
|
|
32
|
+
cacheDir: ".git/.aicc-cache",
|
|
33
|
+
plugins: [],
|
|
34
|
+
verbose: process.env.AICC_VERBOSE === "true"
|
|
35
|
+
};
|
|
36
|
+
function getGlobalConfigPath() {
|
|
37
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
38
|
+
return resolve(base, "ai-conventional-commit-cli", "aicc.json");
|
|
39
|
+
}
|
|
40
|
+
function saveGlobalConfig(partial) {
|
|
41
|
+
const filePath = getGlobalConfigPath();
|
|
42
|
+
const dir = dirname(filePath);
|
|
43
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
44
|
+
let existing = {};
|
|
45
|
+
if (existsSync(filePath)) {
|
|
46
|
+
try {
|
|
47
|
+
existing = JSON.parse(readFileSync(filePath, "utf8")) || {};
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (process.env.AICC_VERBOSE === "true") {
|
|
50
|
+
console.error("[ai-cc] Failed to parse existing global config, overwriting.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const merged = { ...existing, ...partial };
|
|
55
|
+
writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
56
|
+
return filePath;
|
|
57
|
+
}
|
|
58
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
59
|
+
return (await loadConfigDetailed(cwd)).config;
|
|
60
|
+
}
|
|
61
|
+
async function loadConfigDetailed(cwd = process.cwd()) {
|
|
62
|
+
let globalCfg = {};
|
|
63
|
+
const globalPath = getGlobalConfigPath();
|
|
64
|
+
if (existsSync(globalPath)) {
|
|
65
|
+
try {
|
|
66
|
+
globalCfg = JSON.parse(readFileSync(globalPath, "utf8")) || {};
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (process.env.AICC_VERBOSE === "true") {
|
|
69
|
+
console.error("[ai-cc] Failed to parse global config, ignoring.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const explorer = cosmiconfig("aicc");
|
|
74
|
+
const result = await explorer.search(cwd);
|
|
75
|
+
const projectCfg = result?.config || {};
|
|
76
|
+
const envCfg = {};
|
|
77
|
+
if (process.env.AICC_MODEL) envCfg.model = process.env.AICC_MODEL;
|
|
78
|
+
if (process.env.AICC_PRIVACY) envCfg.privacy = process.env.AICC_PRIVACY;
|
|
79
|
+
if (process.env.AICC_STYLE) envCfg.style = process.env.AICC_STYLE;
|
|
80
|
+
if (process.env.AICC_STYLE_SAMPLES)
|
|
81
|
+
envCfg.styleSamples = parseInt(process.env.AICC_STYLE_SAMPLES, 10);
|
|
82
|
+
if (process.env.AICC_MAX_TOKENS) envCfg.maxTokens = parseInt(process.env.AICC_MAX_TOKENS, 10);
|
|
83
|
+
if (process.env.AICC_MAX_FILE_LINES)
|
|
84
|
+
envCfg.maxFileLines = parseInt(process.env.AICC_MAX_FILE_LINES, 10);
|
|
85
|
+
if (process.env.AICC_VERBOSE) envCfg.verbose = process.env.AICC_VERBOSE === "true";
|
|
86
|
+
const merged = {
|
|
87
|
+
...DEFAULTS,
|
|
88
|
+
...globalCfg,
|
|
89
|
+
...projectCfg,
|
|
90
|
+
...envCfg
|
|
91
|
+
};
|
|
92
|
+
merged.plugins = (merged.plugins || []).filter((p) => {
|
|
93
|
+
const abs = resolve(cwd, p);
|
|
94
|
+
return existsSync(abs);
|
|
95
|
+
});
|
|
96
|
+
if (!merged.skipFilePatterns) {
|
|
97
|
+
merged.skipFilePatterns = DEFAULTS.skipFilePatterns;
|
|
98
|
+
}
|
|
99
|
+
const sources = Object.keys(merged).reduce((acc, key) => {
|
|
100
|
+
const k = key;
|
|
101
|
+
let src = "default";
|
|
102
|
+
if (k in globalCfg) src = "global";
|
|
103
|
+
if (k in projectCfg) src = "project";
|
|
104
|
+
if (k in envCfg) src = "env";
|
|
105
|
+
acc[k] = src;
|
|
106
|
+
return acc;
|
|
107
|
+
}, {});
|
|
108
|
+
const withMeta = Object.assign(merged, { _sources: sources });
|
|
109
|
+
return {
|
|
110
|
+
config: withMeta,
|
|
111
|
+
raw: { defaults: DEFAULTS, global: globalCfg, project: projectCfg, env: envCfg }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
getGlobalConfigPath,
|
|
117
|
+
saveGlobalConfig,
|
|
118
|
+
loadConfig,
|
|
119
|
+
loadConfigDetailed
|
|
120
|
+
};
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// src/prompt.ts
|
|
2
|
+
var matchesPattern = (filePath, pattern) => {
|
|
3
|
+
const regexPattern = pattern.replace(/\*\*/g, "\xA7DOUBLESTAR\xA7").replace(/\*/g, "[^/]*").replace(/§DOUBLESTAR§/g, ".*").replace(/\./g, "\\.").replace(/\?/g, ".");
|
|
4
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
5
|
+
return regex.test(filePath);
|
|
6
|
+
};
|
|
7
|
+
var summarizeDiffForPrompt = (files, privacy, maxFileLines, skipFilePatterns) => {
|
|
8
|
+
const getTotalLines = (f) => {
|
|
9
|
+
return f.hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
10
|
+
};
|
|
11
|
+
const shouldSkipFile = (filePath) => {
|
|
12
|
+
return skipFilePatterns.some((pattern) => matchesPattern(filePath, pattern));
|
|
13
|
+
};
|
|
14
|
+
if (privacy === "high") {
|
|
15
|
+
return files.map((f) => {
|
|
16
|
+
const totalLines = getTotalLines(f);
|
|
17
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
18
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
19
|
+
const skipped = patternSkipped || sizeSkipped;
|
|
20
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
21
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}${skipped ? ` [${reason}, content skipped]` : ""}`;
|
|
22
|
+
}).join("\n");
|
|
23
|
+
}
|
|
24
|
+
if (privacy === "medium") {
|
|
25
|
+
return files.map((f) => {
|
|
26
|
+
const totalLines = getTotalLines(f);
|
|
27
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
28
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
29
|
+
if (patternSkipped || sizeSkipped) {
|
|
30
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
31
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
32
|
+
}
|
|
33
|
+
return `file: ${f.file}
|
|
34
|
+
` + f.hunks.map(
|
|
35
|
+
(h) => ` hunk ${h.hash} context:${h.functionContext || ""} +${h.added} -${h.removed}`
|
|
36
|
+
).join("\n");
|
|
37
|
+
}).join("\n");
|
|
38
|
+
}
|
|
39
|
+
return files.map((f) => {
|
|
40
|
+
const totalLines = getTotalLines(f);
|
|
41
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
42
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
43
|
+
if (patternSkipped || sizeSkipped) {
|
|
44
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
45
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
46
|
+
}
|
|
47
|
+
return `file: ${f.file}
|
|
48
|
+
` + f.hunks.map(
|
|
49
|
+
(h) => `${h.header}
|
|
50
|
+
${h.lines.slice(0, 40).join("\n")}${h.lines.length > 40 ? "\n[truncated]" : ""}`
|
|
51
|
+
).join("\n");
|
|
52
|
+
}).join("\n");
|
|
53
|
+
};
|
|
54
|
+
var buildGenerationMessages = (opts) => {
|
|
55
|
+
const { files, style, config, mode, desiredCommits } = opts;
|
|
56
|
+
const diff = summarizeDiffForPrompt(
|
|
57
|
+
files,
|
|
58
|
+
config.privacy,
|
|
59
|
+
config.maxFileLines,
|
|
60
|
+
config.skipFilePatterns
|
|
61
|
+
);
|
|
62
|
+
const TYPE_MAP = {
|
|
63
|
+
feat: "A new feature or capability added for the user",
|
|
64
|
+
fix: "A bug fix resolving incorrect behavior",
|
|
65
|
+
chore: "Internal change with no user-facing impact",
|
|
66
|
+
docs: "Documentation-only changes",
|
|
67
|
+
refactor: "Code change that neither fixes a bug nor adds a feature",
|
|
68
|
+
test: "Adding or improving tests only",
|
|
69
|
+
ci: "Changes to CI configuration or scripts",
|
|
70
|
+
perf: "Performance improvement",
|
|
71
|
+
style: "Formatting or stylistic change (no logic)",
|
|
72
|
+
build: "Build system or dependency changes",
|
|
73
|
+
revert: "Revert a previous commit",
|
|
74
|
+
merge: "Merge branches (rare; only if truly a merge commit)",
|
|
75
|
+
security: "Security-related change or hardening",
|
|
76
|
+
release: "Version bump or release meta change"
|
|
77
|
+
};
|
|
78
|
+
const specLines = [];
|
|
79
|
+
specLines.push(
|
|
80
|
+
"Purpose: Generate high-quality Conventional Commit messages for the provided git diff."
|
|
81
|
+
);
|
|
82
|
+
specLines.push("Locale: en");
|
|
83
|
+
specLines.push(
|
|
84
|
+
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[], "files"?: string[] } ], "meta": { "splitRecommended": boolean } }'
|
|
85
|
+
);
|
|
86
|
+
specLines.push("Primary Output Field: commits[ ].title");
|
|
87
|
+
specLines.push("Title Format (REQUIRED): <type>(<scope>): <subject>");
|
|
88
|
+
specLines.push(
|
|
89
|
+
"Title Length Guidance: Aim for <=50 chars ideal; absolute max 72 (do not exceed)."
|
|
90
|
+
);
|
|
91
|
+
specLines.push("Types (JSON mapping follows on next line)");
|
|
92
|
+
specLines.push("TypeMap: " + JSON.stringify(TYPE_MAP));
|
|
93
|
+
specLines.push(
|
|
94
|
+
"Scope Rules: ALWAYS include a concise lowercase kebab-case scope (derive from dominant directory, package, or feature); never omit."
|
|
95
|
+
);
|
|
96
|
+
specLines.push(
|
|
97
|
+
"Subject Rules: imperative mood, present tense, no leading capital unless proper noun, no trailing period."
|
|
98
|
+
);
|
|
99
|
+
specLines.push(
|
|
100
|
+
"Length Rule: Keep titles concise; prefer 50 or fewer chars; MUST be <=72 including type/scope."
|
|
101
|
+
);
|
|
102
|
+
specLines.push(
|
|
103
|
+
"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.")
|
|
104
|
+
);
|
|
105
|
+
specLines.push(
|
|
106
|
+
"Forbidden: breaking changes notation, exclamation mark after type unless truly semver-major (avoid unless diff clearly indicates)."
|
|
107
|
+
);
|
|
108
|
+
specLines.push("Fallback Type: use chore when no other type clearly fits.");
|
|
109
|
+
specLines.push("Consistency: prefer existing top prefixes: " + style.topPrefixes.join(", "));
|
|
110
|
+
specLines.push("Provide score (0-100) measuring clarity & specificity (higher is better).");
|
|
111
|
+
specLines.push(
|
|
112
|
+
"Provide reasons array citing concrete diff elements: filenames, functions, tests, metrics."
|
|
113
|
+
);
|
|
114
|
+
specLines.push(
|
|
115
|
+
'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).'
|
|
116
|
+
);
|
|
117
|
+
specLines.push("Return ONLY the JSON object. No surrounding text or markdown.");
|
|
118
|
+
specLines.push("Do not add fields not listed in schema.");
|
|
119
|
+
specLines.push("Never fabricate content not present or implied by the diff.");
|
|
120
|
+
specLines.push(
|
|
121
|
+
"If mode is split and multiple logical changes exist, set meta.splitRecommended=true."
|
|
122
|
+
);
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
role: "system",
|
|
126
|
+
content: specLines.join("\n")
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
role: "user",
|
|
130
|
+
content: `Mode: ${mode}
|
|
131
|
+
RequestedCommitCount: ${desiredCommits || (mode === "split" ? "2-6" : 1)}
|
|
132
|
+
StyleFingerprint: ${JSON.stringify(style)}
|
|
133
|
+
Diff:
|
|
134
|
+
${diff}
|
|
135
|
+
Generate commit candidates now.`
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
};
|
|
139
|
+
var buildRefineMessages = (opts) => {
|
|
140
|
+
const { originalPlan, index, instructions, config } = opts;
|
|
141
|
+
const target = originalPlan.commits[index];
|
|
142
|
+
const spec = [];
|
|
143
|
+
spec.push("Purpose: Refine a single Conventional Commit message while preserving intent.");
|
|
144
|
+
spec.push("Locale: en");
|
|
145
|
+
spec.push("Input: one existing commit JSON object.");
|
|
146
|
+
spec.push(
|
|
147
|
+
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ] }'
|
|
148
|
+
);
|
|
149
|
+
spec.push("Title Format (REQUIRED): <type>(<scope>): <subject> (<=72 chars)");
|
|
150
|
+
spec.push("Subject: imperative, present tense, no trailing period.");
|
|
151
|
+
spec.push(
|
|
152
|
+
"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.")
|
|
153
|
+
);
|
|
154
|
+
spec.push(
|
|
155
|
+
"Preserve semantic meaning; ensure a scope is present (infer one if missing); only improve clarity, brevity, conformity."
|
|
156
|
+
);
|
|
157
|
+
spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
|
|
158
|
+
spec.push("Return ONLY JSON (commits array length=1).");
|
|
159
|
+
return [
|
|
160
|
+
{ role: "system", content: spec.join("\n") },
|
|
161
|
+
{
|
|
162
|
+
role: "user",
|
|
163
|
+
content: `Current commit object:
|
|
164
|
+
${JSON.stringify(target, null, 2)}
|
|
165
|
+
Instructions:
|
|
166
|
+
${instructions.join("\n") || "None"}
|
|
167
|
+
Refine now.`
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/model/provider.ts
|
|
173
|
+
import { z } from "zod";
|
|
174
|
+
import { execa } from "execa";
|
|
175
|
+
var OpenCodeProvider = class {
|
|
176
|
+
constructor(model = "github-copilot/gpt-4.1") {
|
|
177
|
+
this.model = model;
|
|
178
|
+
}
|
|
179
|
+
name() {
|
|
180
|
+
return "opencode";
|
|
181
|
+
}
|
|
182
|
+
async chat(messages, _opts) {
|
|
183
|
+
const debug = process.env.AICC_DEBUG === "true";
|
|
184
|
+
const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
|
|
185
|
+
const timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
|
|
186
|
+
const eager = process.env.AICC_EAGER_PARSE !== "false";
|
|
187
|
+
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
188
|
+
const command = `Generate high-quality commit message candidates based on the staged git diff.`;
|
|
189
|
+
const fullPrompt = `${command}
|
|
190
|
+
|
|
191
|
+
Context:
|
|
192
|
+
${userAggregate}`;
|
|
193
|
+
if (mockMode) {
|
|
194
|
+
if (debug) console.error("[ai-cc][mock] Returning deterministic mock response");
|
|
195
|
+
return JSON.stringify({
|
|
196
|
+
commits: [
|
|
197
|
+
{
|
|
198
|
+
title: "chore: mock commit from provider",
|
|
199
|
+
body: "",
|
|
200
|
+
score: 80,
|
|
201
|
+
reasons: ["mock mode"]
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
meta: { splitRecommended: false }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
return await new Promise((resolve, reject) => {
|
|
209
|
+
let resolved = false;
|
|
210
|
+
let acc = "";
|
|
211
|
+
const includeLogs = process.env.AICC_PRINT_LOGS === "true";
|
|
212
|
+
const args = ["run", fullPrompt, "--model", this.model];
|
|
213
|
+
if (includeLogs) args.push("--print-logs");
|
|
214
|
+
const subprocess = execa("opencode", args, {
|
|
215
|
+
timeout: timeoutMs,
|
|
216
|
+
input: ""
|
|
217
|
+
// immediately close stdin in case CLI waits for it
|
|
218
|
+
});
|
|
219
|
+
const finish = (value) => {
|
|
220
|
+
if (resolved) return;
|
|
221
|
+
resolved = true;
|
|
222
|
+
const elapsed = Date.now() - start;
|
|
223
|
+
if (debug) {
|
|
224
|
+
console.error(
|
|
225
|
+
`[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
resolve(value);
|
|
229
|
+
};
|
|
230
|
+
const tryEager = () => {
|
|
231
|
+
if (!eager) return;
|
|
232
|
+
const first = acc.indexOf("{");
|
|
233
|
+
const last = acc.lastIndexOf("}");
|
|
234
|
+
if (first !== -1 && last !== -1 && last > first) {
|
|
235
|
+
const candidate = acc.slice(first, last + 1).trim();
|
|
236
|
+
try {
|
|
237
|
+
JSON.parse(candidate);
|
|
238
|
+
if (debug) console.error("[ai-cc][provider] eager JSON detected, terminating process");
|
|
239
|
+
subprocess.kill("SIGTERM");
|
|
240
|
+
finish(candidate);
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
subprocess.stdout?.on("data", (chunk) => {
|
|
246
|
+
const text = chunk.toString();
|
|
247
|
+
acc += text;
|
|
248
|
+
tryEager();
|
|
249
|
+
});
|
|
250
|
+
subprocess.stderr?.on("data", (chunk) => {
|
|
251
|
+
if (debug) console.error("[ai-cc][provider][stderr]", chunk.toString().trim());
|
|
252
|
+
});
|
|
253
|
+
subprocess.then(({ stdout }) => {
|
|
254
|
+
if (!resolved) finish(stdout);
|
|
255
|
+
}).catch((e) => {
|
|
256
|
+
if (resolved) return;
|
|
257
|
+
const elapsed = Date.now() - start;
|
|
258
|
+
if (e.timedOut) {
|
|
259
|
+
return reject(
|
|
260
|
+
new Error(`Model call timed out after ${timeoutMs}ms (elapsed=${elapsed}ms)`)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (debug) console.error("[ai-cc][provider] failure", e.stderr || e.message);
|
|
264
|
+
reject(new Error(e.stderr || e.message || "opencode invocation failed"));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var CommitSchema = z.object({
|
|
270
|
+
title: z.string().min(5).max(150),
|
|
271
|
+
body: z.string().optional().default(""),
|
|
272
|
+
score: z.number().min(0).max(100),
|
|
273
|
+
reasons: z.array(z.string()).optional().default([]),
|
|
274
|
+
files: z.array(z.string()).optional().default([])
|
|
275
|
+
});
|
|
276
|
+
var PlanSchema = z.object({
|
|
277
|
+
commits: z.array(CommitSchema).min(1),
|
|
278
|
+
meta: z.object({
|
|
279
|
+
splitRecommended: z.boolean().optional()
|
|
280
|
+
}).optional()
|
|
281
|
+
});
|
|
282
|
+
var extractJSON = (raw) => {
|
|
283
|
+
const trimmed = raw.trim();
|
|
284
|
+
let jsonText = null;
|
|
285
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
286
|
+
jsonText = trimmed;
|
|
287
|
+
} else {
|
|
288
|
+
const match = raw.match(/\{[\s\S]*\}$/m);
|
|
289
|
+
if (match) jsonText = match[0];
|
|
290
|
+
}
|
|
291
|
+
if (!jsonText) throw new Error("No JSON object detected.");
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(jsonText);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
throw new Error("Invalid JSON parse");
|
|
297
|
+
}
|
|
298
|
+
return PlanSchema.parse(parsed);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/guardrails.ts
|
|
302
|
+
var SECRET_PATTERNS = [
|
|
303
|
+
/AWS_[A-Z0-9_]+/i,
|
|
304
|
+
/BEGIN RSA PRIVATE KEY/,
|
|
305
|
+
/-----BEGIN PRIVATE KEY-----/,
|
|
306
|
+
/ssh-rsa AAAA/
|
|
307
|
+
];
|
|
308
|
+
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;
|
|
309
|
+
var sanitizeTitle = (title, allowEmoji) => {
|
|
310
|
+
let t = title.trim();
|
|
311
|
+
if (allowEmoji) {
|
|
312
|
+
const multi = t.match(/^((?:[\p{Emoji}\p{So}\p{Sk}]+)[\p{Emoji}\p{So}\p{Sk}\s]*)+/u);
|
|
313
|
+
if (multi) {
|
|
314
|
+
const first = Array.from(multi[0].trim())[0];
|
|
315
|
+
t = first + " " + t.slice(multi[0].length).trimStart();
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
t = t.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trimStart();
|
|
319
|
+
}
|
|
320
|
+
return t;
|
|
321
|
+
};
|
|
322
|
+
var normalizeConventionalTitle = (title) => {
|
|
323
|
+
let original = title.trim();
|
|
324
|
+
let leadingEmoji = "";
|
|
325
|
+
const emojiCluster = original.match(/^[\p{Emoji}\p{So}\p{Sk}]+/u);
|
|
326
|
+
if (emojiCluster) {
|
|
327
|
+
leadingEmoji = Array.from(emojiCluster[0])[0];
|
|
328
|
+
}
|
|
329
|
+
let t = original.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trim();
|
|
330
|
+
const m = t.match(/^(\w+)(\(.+\))?:\s+(.*)$/);
|
|
331
|
+
let result;
|
|
332
|
+
if (m) {
|
|
333
|
+
const type = m[1].toLowerCase();
|
|
334
|
+
const scope = m[2] || "";
|
|
335
|
+
let subject = m[3].trim();
|
|
336
|
+
subject = subject.replace(/\.$/, "");
|
|
337
|
+
subject = subject.charAt(0).toLowerCase() + subject.slice(1);
|
|
338
|
+
result = `${type}${scope}: ${subject}`;
|
|
339
|
+
} else if (!/^\w+\(.+\)?: /.test(t)) {
|
|
340
|
+
t = t.replace(/\.$/, "");
|
|
341
|
+
t = t.charAt(0).toLowerCase() + t.slice(1);
|
|
342
|
+
result = `chore: ${t}`;
|
|
343
|
+
} else {
|
|
344
|
+
result = t;
|
|
345
|
+
}
|
|
346
|
+
if (leadingEmoji) {
|
|
347
|
+
result = `${leadingEmoji} ${result}`;
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
var checkCandidate = (candidate) => {
|
|
352
|
+
const errs = [];
|
|
353
|
+
if (!CONVENTIONAL_RE.test(candidate.title)) {
|
|
354
|
+
errs.push("Not a valid conventional commit title.");
|
|
355
|
+
}
|
|
356
|
+
if (/^[A-Z]/.test(candidate.title)) {
|
|
357
|
+
}
|
|
358
|
+
const body = candidate.body || "";
|
|
359
|
+
for (const pat of SECRET_PATTERNS) {
|
|
360
|
+
if (pat.test(body)) {
|
|
361
|
+
errs.push("Potential secret detected.");
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return errs;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/title-format.ts
|
|
369
|
+
var EMOJI_MAP = {
|
|
370
|
+
feat: "\u2728",
|
|
371
|
+
fix: "\u{1F41B}",
|
|
372
|
+
chore: "\u{1F9F9}",
|
|
373
|
+
docs: "\u{1F4DD}",
|
|
374
|
+
refactor: "\u267B\uFE0F",
|
|
375
|
+
test: "\u2705",
|
|
376
|
+
ci: "\u{1F916}",
|
|
377
|
+
perf: "\u26A1\uFE0F",
|
|
378
|
+
style: "\u{1F3A8}",
|
|
379
|
+
build: "\u{1F3D7}\uFE0F",
|
|
380
|
+
revert: "\u23EA",
|
|
381
|
+
merge: "\u{1F500}",
|
|
382
|
+
security: "\u{1F512}",
|
|
383
|
+
release: "\u{1F3F7}\uFE0F"
|
|
384
|
+
};
|
|
385
|
+
var EMOJI_TYPE_RE = /^([\p{Emoji}\p{So}\p{Sk}])\s+(\w+)(\(.+\))?:\s+(.*)$/u;
|
|
386
|
+
var TYPE_RE = /^(\w+)(\(.+\))?:\s+(.*)$/;
|
|
387
|
+
var formatCommitTitle = (raw, opts) => {
|
|
388
|
+
const { allowGitmoji, mode = "standard" } = opts;
|
|
389
|
+
let norm = normalizeConventionalTitle(sanitizeTitle(raw, allowGitmoji));
|
|
390
|
+
if (!allowGitmoji || mode !== "gitmoji" && mode !== "gitmoji-pure") {
|
|
391
|
+
return norm;
|
|
392
|
+
}
|
|
393
|
+
if (mode === "gitmoji-pure") {
|
|
394
|
+
let m2 = norm.match(EMOJI_TYPE_RE);
|
|
395
|
+
if (m2) {
|
|
396
|
+
const emoji = m2[1];
|
|
397
|
+
const subject = m2[4];
|
|
398
|
+
norm = `${emoji}: ${subject}`;
|
|
399
|
+
} else if (m2 = norm.match(TYPE_RE)) {
|
|
400
|
+
const type = m2[1];
|
|
401
|
+
const subject = m2[3];
|
|
402
|
+
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
403
|
+
norm = `${em}: ${subject}`;
|
|
404
|
+
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+:/u.test(norm)) {
|
|
405
|
+
norm = `\u{1F527}: ${norm}`;
|
|
406
|
+
}
|
|
407
|
+
return norm;
|
|
408
|
+
}
|
|
409
|
+
let m = norm.match(EMOJI_TYPE_RE);
|
|
410
|
+
if (m) {
|
|
411
|
+
return norm;
|
|
412
|
+
}
|
|
413
|
+
if (m = norm.match(TYPE_RE)) {
|
|
414
|
+
const type = m[1];
|
|
415
|
+
const scope = m[2] || "";
|
|
416
|
+
const subject = m[3];
|
|
417
|
+
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
418
|
+
norm = `${em} ${type}${scope}: ${subject}`;
|
|
419
|
+
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+\s+\w+.*:/u.test(norm)) {
|
|
420
|
+
norm = `\u{1F527} chore: ${norm}`;
|
|
421
|
+
}
|
|
422
|
+
return norm;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/workflow/ui.ts
|
|
426
|
+
import chalk from "chalk";
|
|
427
|
+
function animateHeaderBase(text = "ai-conventional-commit", modelSegment) {
|
|
428
|
+
const mainText = text;
|
|
429
|
+
const modelSeg = modelSegment ? ` (using ${modelSegment})` : "";
|
|
430
|
+
if (!process.stdout.isTTY || process.env.AICC_NO_ANIMATION) {
|
|
431
|
+
if (modelSeg) console.log("\n\u250C " + chalk.bold(mainText) + chalk.dim(modelSeg));
|
|
432
|
+
else console.log("\n\u250C " + chalk.bold(mainText));
|
|
433
|
+
return Promise.resolve();
|
|
434
|
+
}
|
|
435
|
+
const palette = [
|
|
436
|
+
"#3a0d6d",
|
|
437
|
+
"#5a1ea3",
|
|
438
|
+
"#7a32d6",
|
|
439
|
+
"#9a4dff",
|
|
440
|
+
"#b267ff",
|
|
441
|
+
"#c37dff",
|
|
442
|
+
"#b267ff",
|
|
443
|
+
"#9a4dff",
|
|
444
|
+
"#7a32d6",
|
|
445
|
+
"#5a1ea3"
|
|
446
|
+
];
|
|
447
|
+
process.stdout.write("\n");
|
|
448
|
+
return palette.reduce(async (p, color) => {
|
|
449
|
+
await p;
|
|
450
|
+
const frame = chalk.bold.hex(color)(mainText);
|
|
451
|
+
if (modelSeg) process.stdout.write("\r\u250C " + frame + chalk.dim(modelSeg));
|
|
452
|
+
else process.stdout.write("\r\u250C " + frame);
|
|
453
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
454
|
+
}, Promise.resolve()).then(() => process.stdout.write("\n"));
|
|
455
|
+
}
|
|
456
|
+
function borderLine(content) {
|
|
457
|
+
if (!content) console.log("\u2502");
|
|
458
|
+
else console.log("\u2502 " + content);
|
|
459
|
+
}
|
|
460
|
+
function sectionTitle(label) {
|
|
461
|
+
console.log("\u2299 " + chalk.bold(label));
|
|
462
|
+
}
|
|
463
|
+
function abortMessage() {
|
|
464
|
+
console.log("\u2514 \u{1F645}\u200D\u2640\uFE0F No commit created.");
|
|
465
|
+
console.log();
|
|
466
|
+
}
|
|
467
|
+
function finalSuccess(opts) {
|
|
468
|
+
const elapsedMs = Date.now() - opts.startedAt;
|
|
469
|
+
const seconds = elapsedMs / 1e3;
|
|
470
|
+
const dur = seconds >= 0.1 ? seconds.toFixed(1) + "s" : elapsedMs + "ms";
|
|
471
|
+
const plural = opts.count !== 1;
|
|
472
|
+
if (plural) console.log(`\u2514 \u2728 ${opts.count} commits created in ${dur}.`);
|
|
473
|
+
else console.log(`\u2514 \u2728 commit created in ${dur}.`);
|
|
474
|
+
console.log();
|
|
475
|
+
}
|
|
476
|
+
function createPhasedSpinner(oraLib) {
|
|
477
|
+
const useAnim = process.stdout.isTTY && !process.env.AICC_NO_ANIMATION && !process.env.AICC_NO_SPINNER_ANIM;
|
|
478
|
+
const palette = [
|
|
479
|
+
"#3a0d6d",
|
|
480
|
+
"#5a1ea3",
|
|
481
|
+
"#7a32d6",
|
|
482
|
+
"#9a4dff",
|
|
483
|
+
"#b267ff",
|
|
484
|
+
"#c37dff",
|
|
485
|
+
"#b267ff",
|
|
486
|
+
"#9a4dff",
|
|
487
|
+
"#7a32d6",
|
|
488
|
+
"#5a1ea3"
|
|
489
|
+
];
|
|
490
|
+
let label = "Starting";
|
|
491
|
+
let i = 0;
|
|
492
|
+
const spinner = oraLib({ text: chalk.bold(label), spinner: "dots" }).start();
|
|
493
|
+
let interval = null;
|
|
494
|
+
function frame() {
|
|
495
|
+
if (!useAnim) return;
|
|
496
|
+
spinner.text = chalk.bold.hex(palette[i])(label);
|
|
497
|
+
i = (i + 1) % palette.length;
|
|
498
|
+
}
|
|
499
|
+
if (useAnim) {
|
|
500
|
+
frame();
|
|
501
|
+
interval = setInterval(frame, 80);
|
|
502
|
+
}
|
|
503
|
+
function setLabel(next) {
|
|
504
|
+
label = next;
|
|
505
|
+
if (useAnim) {
|
|
506
|
+
i = 0;
|
|
507
|
+
frame();
|
|
508
|
+
} else {
|
|
509
|
+
spinner.text = chalk.bold(label);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function stopAnim() {
|
|
513
|
+
if (interval) {
|
|
514
|
+
clearInterval(interval);
|
|
515
|
+
interval = null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
spinner,
|
|
520
|
+
async step(l, fn) {
|
|
521
|
+
setLabel(l);
|
|
522
|
+
try {
|
|
523
|
+
return await fn();
|
|
524
|
+
} catch (e) {
|
|
525
|
+
stopAnim();
|
|
526
|
+
const msg = `${l} failed: ${e?.message || e}`.replace(/^\s+/, "");
|
|
527
|
+
spinner.fail(msg);
|
|
528
|
+
throw e;
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
phase(l) {
|
|
532
|
+
setLabel(l);
|
|
533
|
+
},
|
|
534
|
+
stop() {
|
|
535
|
+
stopAnim();
|
|
536
|
+
spinner.stop();
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function renderCommitBlock(opts) {
|
|
541
|
+
const dim = (s) => chalk.dim(s);
|
|
542
|
+
const white = (s) => chalk.white(s);
|
|
543
|
+
const msgColor = opts.messageLabelColor || dim;
|
|
544
|
+
const descColor = opts.descriptionLabelColor || dim;
|
|
545
|
+
const titleColor = opts.titleColor || white;
|
|
546
|
+
const bodyFirst = opts.bodyFirstLineColor || white;
|
|
547
|
+
const bodyRest = opts.bodyLineColor || white;
|
|
548
|
+
if (opts.fancy) {
|
|
549
|
+
const heading = opts.heading ? chalk.hex("#9a4dff").bold(opts.heading) : void 0;
|
|
550
|
+
if (heading) borderLine(heading);
|
|
551
|
+
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
552
|
+
} else {
|
|
553
|
+
if (opts.heading) borderLine(chalk.bold(opts.heading));
|
|
554
|
+
if (!opts.hideMessageLabel)
|
|
555
|
+
borderLine(msgColor("Message:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
556
|
+
else
|
|
557
|
+
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
558
|
+
}
|
|
559
|
+
borderLine();
|
|
560
|
+
if (opts.body) {
|
|
561
|
+
const lines = opts.body.split("\n");
|
|
562
|
+
lines.forEach((line, i) => {
|
|
563
|
+
if (line.trim().length === 0) borderLine();
|
|
564
|
+
else if (i === 0) {
|
|
565
|
+
borderLine(descColor("Description:"));
|
|
566
|
+
borderLine(bodyFirst(line));
|
|
567
|
+
} else borderLine(bodyRest(line));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export {
|
|
573
|
+
buildGenerationMessages,
|
|
574
|
+
buildRefineMessages,
|
|
575
|
+
OpenCodeProvider,
|
|
576
|
+
extractJSON,
|
|
577
|
+
checkCandidate,
|
|
578
|
+
formatCommitTitle,
|
|
579
|
+
animateHeaderBase,
|
|
580
|
+
borderLine,
|
|
581
|
+
sectionTitle,
|
|
582
|
+
abortMessage,
|
|
583
|
+
finalSuccess,
|
|
584
|
+
createPhasedSpinner,
|
|
585
|
+
renderCommitBlock
|
|
586
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HJR5M6U7.js";
|
|
5
5
|
import {
|
|
6
6
|
OpenCodeProvider,
|
|
7
7
|
abortMessage,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
formatCommitTitle,
|
|
17
17
|
renderCommitBlock,
|
|
18
18
|
sectionTitle
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-WW3N76NL.js";
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
import { Cli, Command, Option } from "clipanion";
|
|
@@ -854,7 +854,7 @@ var ModelsCommand = class extends Command {
|
|
|
854
854
|
});
|
|
855
855
|
async execute() {
|
|
856
856
|
if (this.current) {
|
|
857
|
-
const { loadConfigDetailed } = await import("./config-
|
|
857
|
+
const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
|
|
858
858
|
const { config } = await loadConfigDetailed();
|
|
859
859
|
this.context.stdout.write(`${config.model} (source: ${config._sources.model})
|
|
860
860
|
`);
|
|
@@ -898,7 +898,7 @@ var ModelsCommand = class extends Command {
|
|
|
898
898
|
this.context.stdout.write(model + "\n");
|
|
899
899
|
if (this.save) {
|
|
900
900
|
try {
|
|
901
|
-
const { saveGlobalConfig } = await import("./config-
|
|
901
|
+
const { saveGlobalConfig } = await import("./config-C3S4LWLD.js");
|
|
902
902
|
const path = saveGlobalConfig({ model });
|
|
903
903
|
this.context.stdout.write(`Saved as default model in ${path}
|
|
904
904
|
`);
|
|
@@ -933,7 +933,7 @@ var ConfigShowCommand = class extends Command {
|
|
|
933
933
|
});
|
|
934
934
|
json = Option.Boolean("--json", false, { description: "Output JSON including _sources" });
|
|
935
935
|
async execute() {
|
|
936
|
-
const { loadConfigDetailed } = await import("./config-
|
|
936
|
+
const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
|
|
937
937
|
const { config, raw } = await loadConfigDetailed();
|
|
938
938
|
if (this.json) {
|
|
939
939
|
this.context.stdout.write(JSON.stringify({ config, raw }, null, 2) + "\n");
|
|
@@ -958,7 +958,7 @@ var ConfigGetCommand = class extends Command {
|
|
|
958
958
|
key = Option.String();
|
|
959
959
|
withSource = Option.Boolean("--with-source", false, { description: "Append source label" });
|
|
960
960
|
async execute() {
|
|
961
|
-
const { loadConfigDetailed } = await import("./config-
|
|
961
|
+
const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
|
|
962
962
|
const { config } = await loadConfigDetailed();
|
|
963
963
|
const key = this.key;
|
|
964
964
|
if (!(key in config)) {
|
|
@@ -1006,7 +1006,7 @@ var ConfigSetCommand = class extends Command {
|
|
|
1006
1006
|
} catch {
|
|
1007
1007
|
}
|
|
1008
1008
|
}
|
|
1009
|
-
const { saveGlobalConfig } = await import("./config-
|
|
1009
|
+
const { saveGlobalConfig } = await import("./config-C3S4LWLD.js");
|
|
1010
1010
|
const path = saveGlobalConfig({ [this.key]: parsed });
|
|
1011
1011
|
this.context.stdout.write(`Saved ${this.key} to ${path}
|
|
1012
1012
|
`);
|
|
@@ -1027,7 +1027,7 @@ var RewordCommand = class extends Command {
|
|
|
1027
1027
|
style = Option.String("--style", { required: false, description: "Title style override" });
|
|
1028
1028
|
model = Option.String("-m,--model", { required: false, description: "Model override" });
|
|
1029
1029
|
async execute() {
|
|
1030
|
-
const { runReword } = await import("./reword-
|
|
1030
|
+
const { runReword } = await import("./reword-Q7MES34W.js");
|
|
1031
1031
|
const config = await loadConfig();
|
|
1032
1032
|
if (this.style) config.style = this.style;
|
|
1033
1033
|
if (this.model) config.model = this.model;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OpenCodeProvider,
|
|
3
|
+
abortMessage,
|
|
4
|
+
animateHeaderBase,
|
|
5
|
+
borderLine,
|
|
6
|
+
buildRefineMessages,
|
|
7
|
+
createPhasedSpinner,
|
|
8
|
+
extractJSON,
|
|
9
|
+
finalSuccess,
|
|
10
|
+
formatCommitTitle,
|
|
11
|
+
renderCommitBlock,
|
|
12
|
+
sectionTitle
|
|
13
|
+
} from "./chunk-WW3N76NL.js";
|
|
14
|
+
|
|
15
|
+
// src/workflow/reword.ts
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import ora from "ora";
|
|
18
|
+
import inquirer from "inquirer";
|
|
19
|
+
import { simpleGit } from "simple-git";
|
|
20
|
+
var git = simpleGit();
|
|
21
|
+
async function getCommitMessage(hash) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await git.show([`${hash}`, "--quiet", "--format=%P%n%B"]);
|
|
24
|
+
const lines = raw.split("\n");
|
|
25
|
+
const parentsLine = lines.shift() || "";
|
|
26
|
+
const parents = parentsLine.trim().length ? parentsLine.trim().split(/\s+/) : [];
|
|
27
|
+
const message = lines.join("\n").trim();
|
|
28
|
+
if (!message) return null;
|
|
29
|
+
const [first, ...rest] = message.split("\n");
|
|
30
|
+
const body = rest.join("\n").trim() || void 0;
|
|
31
|
+
return { title: first, body, parents };
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function isAncestor(ancestor, head) {
|
|
37
|
+
try {
|
|
38
|
+
const mb = (await git.raw(["merge-base", ancestor, head])).trim();
|
|
39
|
+
const anc = (await git.revparse([ancestor])).trim();
|
|
40
|
+
return mb === anc;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function runReword(config, hash) {
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
const commit = await getCommitMessage(hash);
|
|
48
|
+
if (!commit) {
|
|
49
|
+
console.log(`Commit not found: ${hash}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (commit.parents.length > 1) {
|
|
53
|
+
console.log("Refusing to reword a merge commit (multiple parents).");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (process.stdout.isTTY) {
|
|
57
|
+
await animateHeaderBase("ai-conventional-commit", config.model);
|
|
58
|
+
borderLine();
|
|
59
|
+
}
|
|
60
|
+
sectionTitle("Original commit");
|
|
61
|
+
borderLine(chalk.yellow(commit.title));
|
|
62
|
+
if (commit.body) {
|
|
63
|
+
commit.body.split("\n").forEach((l) => l.trim().length ? borderLine(l) : borderLine());
|
|
64
|
+
}
|
|
65
|
+
borderLine();
|
|
66
|
+
const instructions = [
|
|
67
|
+
"Improve clarity & conformity to Conventional Commits while preserving meaning."
|
|
68
|
+
];
|
|
69
|
+
const syntheticPlan = {
|
|
70
|
+
commits: [
|
|
71
|
+
{
|
|
72
|
+
title: commit.title,
|
|
73
|
+
body: commit.body,
|
|
74
|
+
score: 0,
|
|
75
|
+
reasons: []
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
const provider = new OpenCodeProvider(config.model);
|
|
80
|
+
const phased = createPhasedSpinner(ora);
|
|
81
|
+
let refined = null;
|
|
82
|
+
try {
|
|
83
|
+
phased.phase("Preparing prompt");
|
|
84
|
+
const messages = buildRefineMessages({
|
|
85
|
+
originalPlan: syntheticPlan,
|
|
86
|
+
index: 0,
|
|
87
|
+
instructions,
|
|
88
|
+
config
|
|
89
|
+
});
|
|
90
|
+
phased.phase("Calling model");
|
|
91
|
+
const raw = await provider.chat(messages, { maxTokens: config.maxTokens });
|
|
92
|
+
phased.phase("Parsing response");
|
|
93
|
+
refined = await extractJSON(raw);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
phased.spinner.fail("Reword failed: " + (e?.message || e));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
phased.stop();
|
|
99
|
+
if (!refined || !refined.commits.length) {
|
|
100
|
+
console.log("No refined commit produced.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const candidate = refined.commits[0];
|
|
104
|
+
candidate.title = formatCommitTitle(candidate.title, {
|
|
105
|
+
allowGitmoji: config.style === "gitmoji" || config.style === "gitmoji-pure",
|
|
106
|
+
mode: config.style
|
|
107
|
+
});
|
|
108
|
+
sectionTitle("Proposed commit");
|
|
109
|
+
renderCommitBlock({
|
|
110
|
+
title: chalk.yellow(candidate.title),
|
|
111
|
+
body: candidate.body,
|
|
112
|
+
hideMessageLabel: true
|
|
113
|
+
});
|
|
114
|
+
borderLine();
|
|
115
|
+
const resolvedHash = (await git.revparse([hash])).trim();
|
|
116
|
+
const headHash = (await git.revparse(["HEAD"])).trim();
|
|
117
|
+
const isHead = headHash === resolvedHash || headHash.startsWith(resolvedHash);
|
|
118
|
+
const { ok } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "list",
|
|
121
|
+
name: "ok",
|
|
122
|
+
message: isHead ? "Amend HEAD with this message?" : "Apply rewrite (history will change)?",
|
|
123
|
+
choices: [
|
|
124
|
+
{ name: "Yes", value: true },
|
|
125
|
+
{ name: "No", value: false }
|
|
126
|
+
],
|
|
127
|
+
default: 0
|
|
128
|
+
}
|
|
129
|
+
]);
|
|
130
|
+
if (!ok) {
|
|
131
|
+
borderLine();
|
|
132
|
+
abortMessage();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const full = candidate.body ? `${candidate.title}
|
|
136
|
+
|
|
137
|
+
${candidate.body}` : candidate.title;
|
|
138
|
+
if (isHead) {
|
|
139
|
+
try {
|
|
140
|
+
await git.commit(full, { "--amend": null });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
borderLine("Failed to amend HEAD: " + (e?.message || e));
|
|
143
|
+
borderLine();
|
|
144
|
+
abortMessage();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
borderLine();
|
|
148
|
+
finalSuccess({ count: 1, startedAt });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const ancestorOk = await isAncestor(resolvedHash, headHash);
|
|
152
|
+
if (!ancestorOk) {
|
|
153
|
+
borderLine("Selected commit is not an ancestor of HEAD.");
|
|
154
|
+
borderLine("Cannot safely rewrite automatically.");
|
|
155
|
+
borderLine();
|
|
156
|
+
abortMessage();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let mergesRange = "";
|
|
160
|
+
try {
|
|
161
|
+
mergesRange = (await git.raw(["rev-list", "--merges", `${resolvedHash}..HEAD`])).trim();
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
if (mergesRange) {
|
|
165
|
+
sectionTitle("Unsafe automatic rewrite");
|
|
166
|
+
borderLine("Merge commits detected between target and HEAD.");
|
|
167
|
+
borderLine("Falling back to manual instructions (preserving previous behavior).");
|
|
168
|
+
borderLine();
|
|
169
|
+
sectionTitle("Apply manually");
|
|
170
|
+
borderLine(`1. git rebase -i ${resolvedHash}~1 --reword`);
|
|
171
|
+
borderLine("2. Mark the line as reword if needed.");
|
|
172
|
+
borderLine("3. Replace the message with:");
|
|
173
|
+
borderLine();
|
|
174
|
+
borderLine(candidate.title);
|
|
175
|
+
if (candidate.body) candidate.body.split("\n").forEach((l) => borderLine(l || void 0));
|
|
176
|
+
borderLine();
|
|
177
|
+
abortMessage();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const tree = (await git.raw(["show", "-s", "--format=%T", resolvedHash])).trim();
|
|
182
|
+
const parent = commit.parents[0];
|
|
183
|
+
const args = ["commit-tree", tree];
|
|
184
|
+
if (parent) args.push("-p", parent);
|
|
185
|
+
args.push("-m", full);
|
|
186
|
+
const newHash = (await git.raw(args)).trim();
|
|
187
|
+
const currentBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
188
|
+
const rebaseTarget = currentBranch === "HEAD" ? "HEAD" : currentBranch;
|
|
189
|
+
await git.raw(["rebase", "--onto", newHash, resolvedHash, rebaseTarget]);
|
|
190
|
+
const afterBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
191
|
+
if (afterBranch === "HEAD" && rebaseTarget !== "HEAD") {
|
|
192
|
+
try {
|
|
193
|
+
await git.checkout([rebaseTarget]);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
sectionTitle("Updated commit");
|
|
198
|
+
borderLine(`Rewrote ${resolvedHash.slice(0, 7)} \u2192 ${newHash.slice(0, 7)}`);
|
|
199
|
+
renderCommitBlock({ title: candidate.title, body: candidate.body, hideMessageLabel: true });
|
|
200
|
+
borderLine();
|
|
201
|
+
finalSuccess({ count: 1, startedAt });
|
|
202
|
+
} catch (e) {
|
|
203
|
+
borderLine("Automatic rewrite failed: " + (e?.message || e));
|
|
204
|
+
borderLine("If a rebase is in progress, resolve conflicts then run: git rebase --continue");
|
|
205
|
+
borderLine("Or abort with: git rebase --abort");
|
|
206
|
+
borderLine();
|
|
207
|
+
abortMessage();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
runReword
|
|
212
|
+
};
|
package/package.json
CHANGED