@quikcommit/cli 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2223 -944
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -72,6 +72,70 @@ var init_tokens = __esm({
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// ../shared/dist/branch.js
|
|
76
|
+
function validateBranchName(name) {
|
|
77
|
+
if (typeof name !== "string")
|
|
78
|
+
return false;
|
|
79
|
+
if (name.length > MAX_BRANCH_NAME_LENGTH)
|
|
80
|
+
return false;
|
|
81
|
+
if (!BRANCH_NAME_RX.test(name))
|
|
82
|
+
return false;
|
|
83
|
+
if (PROTECTED_BRANCH_RX.test(name))
|
|
84
|
+
return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function slugifyFilename(path) {
|
|
88
|
+
const basename = path.split("/").pop() ?? path;
|
|
89
|
+
const noExt = basename.replace(/\.[^.]+$/, "");
|
|
90
|
+
return noExt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
91
|
+
}
|
|
92
|
+
function deterministicBranchName(opts) {
|
|
93
|
+
const files = opts.files ?? [];
|
|
94
|
+
const haystack = `${opts.description ?? ""} ${files.join(" ")}`;
|
|
95
|
+
let type = "chore";
|
|
96
|
+
for (const [rx, t] of TYPE_HINTS) {
|
|
97
|
+
if (rx.test(haystack)) {
|
|
98
|
+
type = t;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let slug;
|
|
103
|
+
if (opts.description) {
|
|
104
|
+
slug = opts.description.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").slice(0, 5).join("-").slice(0, 40);
|
|
105
|
+
} else if (files.length > 0) {
|
|
106
|
+
slug = slugifyFilename(files[0] ?? "");
|
|
107
|
+
} else {
|
|
108
|
+
slug = "changes";
|
|
109
|
+
}
|
|
110
|
+
if (!slug)
|
|
111
|
+
slug = "changes";
|
|
112
|
+
if (slug.length === 1)
|
|
113
|
+
slug = `${slug}-changes`;
|
|
114
|
+
const name = `${type}/${slug}`;
|
|
115
|
+
if (!validateBranchName(name)) {
|
|
116
|
+
return { type: "chore", slug: "updates", name: "chore/updates" };
|
|
117
|
+
}
|
|
118
|
+
return { name, type, slug };
|
|
119
|
+
}
|
|
120
|
+
var BRANCH_NAME_RX, PROTECTED_BRANCH_RX, MAX_BRANCH_NAME_LENGTH, TYPE_HINTS;
|
|
121
|
+
var init_branch = __esm({
|
|
122
|
+
"../shared/dist/branch.js"() {
|
|
123
|
+
"use strict";
|
|
124
|
+
BRANCH_NAME_RX = /^(feat|fix|refactor|perf|docs|test|chore|ci)\/[a-z0-9][a-z0-9-]{0,51}$/;
|
|
125
|
+
PROTECTED_BRANCH_RX = /(^|[/-])(main|master|develop|trunk|release)([/-]|$)/i;
|
|
126
|
+
MAX_BRANCH_NAME_LENGTH = 60;
|
|
127
|
+
TYPE_HINTS = [
|
|
128
|
+
[/\btest|spec\b/i, "test"],
|
|
129
|
+
[/\bdocs?\b|readme|\.md$/i, "docs"],
|
|
130
|
+
[/\bperf|benchmark/i, "perf"],
|
|
131
|
+
[/\brefactor\b/i, "refactor"],
|
|
132
|
+
[/\bci|workflow|github\/actions/i, "ci"],
|
|
133
|
+
[/\bfix|bug|issue/i, "fix"],
|
|
134
|
+
[/\bfeat|add|new\b/i, "feat"]
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
75
139
|
// ../shared/dist/index.js
|
|
76
140
|
var init_dist = __esm({
|
|
77
141
|
"../shared/dist/index.js"() {
|
|
@@ -80,10 +144,19 @@ var init_dist = __esm({
|
|
|
80
144
|
init_constants();
|
|
81
145
|
init_rules();
|
|
82
146
|
init_tokens();
|
|
147
|
+
init_branch();
|
|
83
148
|
}
|
|
84
149
|
});
|
|
85
150
|
|
|
86
151
|
// src/config.ts
|
|
152
|
+
var config_exports = {};
|
|
153
|
+
__export(config_exports, {
|
|
154
|
+
clearApiKey: () => clearApiKey,
|
|
155
|
+
getApiKey: () => getApiKey,
|
|
156
|
+
getConfig: () => getConfig,
|
|
157
|
+
saveApiKey: () => saveApiKey,
|
|
158
|
+
saveConfig: () => saveConfig
|
|
159
|
+
});
|
|
87
160
|
function getApiKey() {
|
|
88
161
|
const envKey = process.env.QC_API_KEY;
|
|
89
162
|
if (envKey?.trim()) return envKey.trim();
|
|
@@ -310,6 +383,13 @@ var init_api = __esm({
|
|
|
310
383
|
body: JSON.stringify(body)
|
|
311
384
|
});
|
|
312
385
|
if (!res.ok) {
|
|
386
|
+
if (res.status === 413) {
|
|
387
|
+
const errBody = await res.json().catch(() => ({}));
|
|
388
|
+
const sizeHint = errBody.received_bytes ? ` (${Math.round(errBody.received_bytes / 1024)}KB > ${Math.round((errBody.limit_bytes ?? 0) / 1024)}KB limit)` : "";
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Diff too large to send${sizeHint}. Try: qc --exclude '*.lock' --exclude 'dist/**' (or commit fewer files at a time).`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
313
393
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
314
394
|
if (planRequiredMsg && err.code === "PLAN_REQUIRED") {
|
|
315
395
|
throw new Error(planRequiredMsg);
|
|
@@ -356,6 +436,9 @@ var init_api = __esm({
|
|
|
356
436
|
summary: data.summary ?? ""
|
|
357
437
|
};
|
|
358
438
|
}
|
|
439
|
+
async generateBranchName(req) {
|
|
440
|
+
return this.request("/v1/branch", req);
|
|
441
|
+
}
|
|
359
442
|
async fetchJson(endpoint, options) {
|
|
360
443
|
if (!this.apiKey) {
|
|
361
444
|
throw new Error("Not authenticated. Run `qc login` first.");
|
|
@@ -7751,6 +7834,9 @@ var require_dist = __commonJS({
|
|
|
7751
7834
|
|
|
7752
7835
|
// src/git.ts
|
|
7753
7836
|
function validateRef(ref, name = "ref") {
|
|
7837
|
+
if (ref.startsWith("-")) {
|
|
7838
|
+
throw new Error(`Invalid git ref ${name}: starts with hyphen: "${ref}"`);
|
|
7839
|
+
}
|
|
7754
7840
|
if (!ref || !SAFE_GIT_REF.test(ref)) {
|
|
7755
7841
|
throw new Error(`Invalid git ref ${name}: "${ref}"`);
|
|
7756
7842
|
}
|
|
@@ -7893,6 +7979,23 @@ function getFullDiff(base = "main") {
|
|
|
7893
7979
|
maxBuffer: 10 * 1024 * 1024
|
|
7894
7980
|
});
|
|
7895
7981
|
}
|
|
7982
|
+
function getStagedDiffShortstat() {
|
|
7983
|
+
try {
|
|
7984
|
+
const out = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--shortstat"], {
|
|
7985
|
+
encoding: "utf-8"
|
|
7986
|
+
}).trim();
|
|
7987
|
+
if (!out) return { additions: 0, deletions: 0 };
|
|
7988
|
+
let additions = 0;
|
|
7989
|
+
let deletions = 0;
|
|
7990
|
+
const ins = /(\d+) insertion/.exec(out);
|
|
7991
|
+
const del = /(\d+) deletion/.exec(out);
|
|
7992
|
+
if (ins?.[1]) additions = parseInt(ins[1], 10);
|
|
7993
|
+
if (del?.[1]) deletions = parseInt(del[1], 10);
|
|
7994
|
+
return { additions, deletions };
|
|
7995
|
+
} catch {
|
|
7996
|
+
return { additions: 0, deletions: 0 };
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7896
7999
|
function getShortStagedFiles(max = 3) {
|
|
7897
8000
|
const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
|
|
7898
8001
|
encoding: "utf-8"
|
|
@@ -7939,6 +8042,105 @@ function getRecentBranchCommits(count = 5) {
|
|
|
7939
8042
|
return [];
|
|
7940
8043
|
}
|
|
7941
8044
|
}
|
|
8045
|
+
function getCommitsAheadOfUpstream(branch, upstream) {
|
|
8046
|
+
validateRef(branch, "branch");
|
|
8047
|
+
const target = upstream ?? `origin/${branch}`;
|
|
8048
|
+
validateRef(target, "upstream");
|
|
8049
|
+
try {
|
|
8050
|
+
const out = (0, import_child_process2.execFileSync)(
|
|
8051
|
+
"git",
|
|
8052
|
+
["rev-list", "--count", `${target}..HEAD`],
|
|
8053
|
+
{ encoding: "utf-8" }
|
|
8054
|
+
).trim();
|
|
8055
|
+
const n = parseInt(out, 10);
|
|
8056
|
+
return Number.isFinite(n) ? n : 0;
|
|
8057
|
+
} catch {
|
|
8058
|
+
return 0;
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
function getUpstreamRef(branch) {
|
|
8062
|
+
validateRef(branch, "branch");
|
|
8063
|
+
try {
|
|
8064
|
+
return (0, import_child_process2.execFileSync)(
|
|
8065
|
+
"git",
|
|
8066
|
+
["rev-parse", "--abbrev-ref", `${branch}@{upstream}`],
|
|
8067
|
+
{ encoding: "utf-8" }
|
|
8068
|
+
).trim() || null;
|
|
8069
|
+
} catch {
|
|
8070
|
+
return null;
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
function getDefaultBranch() {
|
|
8074
|
+
try {
|
|
8075
|
+
const out = (0, import_child_process2.execFileSync)(
|
|
8076
|
+
"git",
|
|
8077
|
+
["symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
8078
|
+
{ encoding: "utf-8" }
|
|
8079
|
+
).trim();
|
|
8080
|
+
const segments = out.split("/");
|
|
8081
|
+
return segments[segments.length - 1] || null;
|
|
8082
|
+
} catch {
|
|
8083
|
+
return null;
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
function branchExists(name) {
|
|
8087
|
+
validateRef(name, "branch");
|
|
8088
|
+
try {
|
|
8089
|
+
(0, import_child_process2.execFileSync)("git", ["show-ref", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
8090
|
+
stdio: "pipe"
|
|
8091
|
+
});
|
|
8092
|
+
return true;
|
|
8093
|
+
} catch {
|
|
8094
|
+
}
|
|
8095
|
+
try {
|
|
8096
|
+
(0, import_child_process2.execFileSync)("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${name}`], {
|
|
8097
|
+
stdio: "pipe"
|
|
8098
|
+
});
|
|
8099
|
+
return true;
|
|
8100
|
+
} catch {
|
|
8101
|
+
return false;
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
function stashPushIfDirty(message) {
|
|
8105
|
+
const status = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], { encoding: "utf-8" }).trim();
|
|
8106
|
+
if (!status) return false;
|
|
8107
|
+
(0, import_child_process2.execFileSync)("git", ["stash", "push", "--include-untracked", "--message", message], {
|
|
8108
|
+
stdio: "pipe"
|
|
8109
|
+
});
|
|
8110
|
+
return true;
|
|
8111
|
+
}
|
|
8112
|
+
function stashPop() {
|
|
8113
|
+
(0, import_child_process2.execFileSync)("git", ["stash", "pop"], { stdio: "pipe" });
|
|
8114
|
+
}
|
|
8115
|
+
function resetHard(ref) {
|
|
8116
|
+
validateRef(ref, "ref");
|
|
8117
|
+
(0, import_child_process2.execFileSync)("git", ["reset", "--hard", ref], { stdio: "pipe" });
|
|
8118
|
+
}
|
|
8119
|
+
function createBranch(name, base = "HEAD") {
|
|
8120
|
+
validateRef(name, "name");
|
|
8121
|
+
validateRef(base, "base");
|
|
8122
|
+
(0, import_child_process2.execFileSync)("git", ["branch", name, base], { stdio: "pipe" });
|
|
8123
|
+
}
|
|
8124
|
+
function checkoutBranch(name) {
|
|
8125
|
+
validateRef(name, "name");
|
|
8126
|
+
(0, import_child_process2.execFileSync)("git", ["checkout", name], { stdio: "pipe" });
|
|
8127
|
+
}
|
|
8128
|
+
function createAndCheckoutBranch(name, base = "HEAD") {
|
|
8129
|
+
validateRef(name, "name");
|
|
8130
|
+
validateRef(base, "base");
|
|
8131
|
+
(0, import_child_process2.execFileSync)("git", ["checkout", "-b", name, base], { stdio: "pipe" });
|
|
8132
|
+
}
|
|
8133
|
+
function getHeadSha() {
|
|
8134
|
+
return (0, import_child_process2.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).trim();
|
|
8135
|
+
}
|
|
8136
|
+
function gitPushSetUpstream(branch) {
|
|
8137
|
+
validateRef(branch, "branch");
|
|
8138
|
+
(0, import_child_process2.execFileSync)("git", ["push", "-u", "origin", branch], { stdio: "inherit" });
|
|
8139
|
+
}
|
|
8140
|
+
function deleteBranch(name) {
|
|
8141
|
+
validateRef(name, "name");
|
|
8142
|
+
(0, import_child_process2.execFileSync)("git", ["branch", "-D", name], { stdio: "pipe" });
|
|
8143
|
+
}
|
|
7942
8144
|
var import_child_process2, import_fs2, import_path2, import_os3, SAFE_GIT_REF;
|
|
7943
8145
|
var init_git = __esm({
|
|
7944
8146
|
"src/git.ts"() {
|
|
@@ -7947,7 +8149,7 @@ var init_git = __esm({
|
|
|
7947
8149
|
import_fs2 = require("fs");
|
|
7948
8150
|
import_path2 = require("path");
|
|
7949
8151
|
import_os3 = require("os");
|
|
7950
|
-
SAFE_GIT_REF = /^[a-zA-Z0-9._\-/~:^@]
|
|
8152
|
+
SAFE_GIT_REF = /^[a-zA-Z0-9][a-zA-Z0-9._\-/~:^@]*$/;
|
|
7951
8153
|
}
|
|
7952
8154
|
});
|
|
7953
8155
|
|
|
@@ -8762,1061 +8964,2053 @@ var init_changeset = __esm({
|
|
|
8762
8964
|
}
|
|
8763
8965
|
});
|
|
8764
8966
|
|
|
8765
|
-
// src/
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
try {
|
|
8773
|
-
hooksDir = (0, import_child_process6.execFileSync)("git", ["rev-parse", "--git-path", "hooks"], {
|
|
8774
|
-
encoding: "utf-8"
|
|
8775
|
-
}).trim();
|
|
8776
|
-
} catch {
|
|
8777
|
-
console.error("Error: Not a git repository");
|
|
8778
|
-
process.exit(1);
|
|
8967
|
+
// src/branch-rescue.ts
|
|
8968
|
+
function rescueCommits(opts) {
|
|
8969
|
+
const upstream = getUpstreamRef(opts.currentBranch);
|
|
8970
|
+
if (!upstream) {
|
|
8971
|
+
throw new Error(
|
|
8972
|
+
`No upstream tracking branch for '${opts.currentBranch}'. Push it first or use \`qc branch\` manually.`
|
|
8973
|
+
);
|
|
8779
8974
|
}
|
|
8780
|
-
const
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8975
|
+
const headSha = getHeadSha();
|
|
8976
|
+
const stashed = stashPushIfDirty(`qc-rescue-${opts.newBranch}`);
|
|
8977
|
+
try {
|
|
8978
|
+
createBranch(opts.newBranch, headSha);
|
|
8979
|
+
} catch (err) {
|
|
8980
|
+
if (stashed) {
|
|
8981
|
+
try {
|
|
8982
|
+
stashPop();
|
|
8983
|
+
} catch {
|
|
8789
8984
|
}
|
|
8790
|
-
} else {
|
|
8791
|
-
console.log("No hook to remove.");
|
|
8792
8985
|
}
|
|
8793
|
-
|
|
8986
|
+
throw err;
|
|
8794
8987
|
}
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8988
|
+
try {
|
|
8989
|
+
resetHard(upstream);
|
|
8990
|
+
} catch (err) {
|
|
8991
|
+
try {
|
|
8992
|
+
deleteBranch(opts.newBranch);
|
|
8993
|
+
} catch {
|
|
8800
8994
|
}
|
|
8801
|
-
|
|
8802
|
-
|
|
8995
|
+
if (stashed) {
|
|
8996
|
+
try {
|
|
8997
|
+
stashPop();
|
|
8998
|
+
} catch {
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
throw new Error(
|
|
9002
|
+
`Rescue aborted: failed to reset ${opts.currentBranch} to upstream. Your repo state has been restored. Original error: ${err?.message ?? String(err)}`
|
|
8803
9003
|
);
|
|
8804
|
-
process.exit(1);
|
|
8805
9004
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
9005
|
+
try {
|
|
9006
|
+
checkoutBranch(opts.newBranch);
|
|
9007
|
+
} catch (err) {
|
|
9008
|
+
try {
|
|
9009
|
+
resetHard(headSha);
|
|
9010
|
+
} catch {
|
|
9011
|
+
}
|
|
9012
|
+
if (stashed) {
|
|
9013
|
+
try {
|
|
9014
|
+
stashPop();
|
|
9015
|
+
} catch {
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
throw err;
|
|
9019
|
+
}
|
|
9020
|
+
if (stashed) {
|
|
9021
|
+
try {
|
|
9022
|
+
stashPop();
|
|
9023
|
+
} catch (err) {
|
|
9024
|
+
throw new Error(
|
|
9025
|
+
`Stash conflict during recovery. Your changes are preserved in the stash entry. Resolve manually with \`git stash pop\` then \`git stash drop\` after resolving conflicts.
|
|
9026
|
+
Original error: ${err?.message ?? err}`
|
|
9027
|
+
);
|
|
9028
|
+
}
|
|
9029
|
+
}
|
|
9030
|
+
return {
|
|
9031
|
+
newBranch: opts.newBranch,
|
|
9032
|
+
stashed,
|
|
9033
|
+
upstreamRef: upstream,
|
|
9034
|
+
movedFromSha: headSha
|
|
9035
|
+
};
|
|
8810
9036
|
}
|
|
8811
|
-
var
|
|
8812
|
-
|
|
8813
|
-
"src/commands/init.ts"() {
|
|
9037
|
+
var init_branch_rescue = __esm({
|
|
9038
|
+
"src/branch-rescue.ts"() {
|
|
8814
9039
|
"use strict";
|
|
8815
|
-
|
|
8816
|
-
import_path8 = require("path");
|
|
8817
|
-
import_child_process6 = require("child_process");
|
|
8818
|
-
HOOK_CONTENT = `#!/bin/sh
|
|
8819
|
-
# Quikcommit - auto-generate commit messages
|
|
8820
|
-
# Installed by: qc init
|
|
8821
|
-
# Remove with: qc init --uninstall
|
|
8822
|
-
|
|
8823
|
-
# Only generate if no message was provided (empty commit message file)
|
|
8824
|
-
COMMIT_MSG_FILE="$1"
|
|
8825
|
-
COMMIT_SOURCE="$2"
|
|
8826
|
-
|
|
8827
|
-
# Skip if message was provided via -m, merge, squash, etc.
|
|
8828
|
-
if [ -n "$COMMIT_SOURCE" ]; then
|
|
8829
|
-
exit 0
|
|
8830
|
-
fi
|
|
8831
|
-
|
|
8832
|
-
# Skip if message file already has content (excluding comments)
|
|
8833
|
-
if grep -qv '^#' "$COMMIT_MSG_FILE" 2>/dev/null; then
|
|
8834
|
-
if [ -n "$(grep -v '^#' "$COMMIT_MSG_FILE" | grep -v '^$')" ]; then
|
|
8835
|
-
exit 0
|
|
8836
|
-
fi
|
|
8837
|
-
fi
|
|
8838
|
-
|
|
8839
|
-
# Generate commit message
|
|
8840
|
-
MSG=$(qc --message-only --hook-mode 2>/dev/null)
|
|
8841
|
-
if [ $? -eq 0 ] && [ -n "$MSG" ]; then
|
|
8842
|
-
printf '%s
|
|
8843
|
-
' "$MSG" > "$COMMIT_MSG_FILE"
|
|
8844
|
-
fi
|
|
8845
|
-
`;
|
|
9040
|
+
init_git();
|
|
8846
9041
|
}
|
|
8847
9042
|
});
|
|
8848
9043
|
|
|
8849
|
-
// src/
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
9044
|
+
// src/branch-detect.ts
|
|
9045
|
+
function matchGlob(name, pattern) {
|
|
9046
|
+
const n = name.toLowerCase();
|
|
9047
|
+
const p = pattern.toLowerCase();
|
|
9048
|
+
if (!p.includes("*")) return n === p;
|
|
9049
|
+
const escaped = p.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
9050
|
+
const withDoubleStar = escaped.replace(/\*\*/g, "\0DOUBLE_STAR\0");
|
|
9051
|
+
const withSingleStar = withDoubleStar.replace(/\*/g, "[^/]*");
|
|
9052
|
+
const final = withSingleStar.replace(/\x00DOUBLE_STAR\x00/g, ".*");
|
|
9053
|
+
const rx = new RegExp("^" + final + "$");
|
|
9054
|
+
return rx.test(n);
|
|
8856
9055
|
}
|
|
8857
|
-
function
|
|
8858
|
-
if (!
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
const
|
|
8863
|
-
if (
|
|
8864
|
-
const
|
|
8865
|
-
|
|
9056
|
+
function isProtectedBranch(branch, protectedList) {
|
|
9057
|
+
if (!protectedList || protectedList.length === 0) return false;
|
|
9058
|
+
return protectedList.some((p) => matchGlob(branch, p));
|
|
9059
|
+
}
|
|
9060
|
+
function resolveProtectedBranches(opts) {
|
|
9061
|
+
const set = /* @__PURE__ */ new Set();
|
|
9062
|
+
if (opts.configList && opts.configList.length > 0) {
|
|
9063
|
+
for (const b of opts.configList) set.add(b);
|
|
9064
|
+
} else {
|
|
9065
|
+
for (const b of HARDCODED_PROTECTED) set.add(b);
|
|
8866
9066
|
}
|
|
8867
|
-
if (
|
|
8868
|
-
|
|
8869
|
-
if (Array.isArray(value)) rules.scopes = value;
|
|
9067
|
+
if (opts.detectDefault && opts.defaultBranch) {
|
|
9068
|
+
set.add(opts.defaultBranch);
|
|
8870
9069
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
9070
|
+
return Array.from(set);
|
|
9071
|
+
}
|
|
9072
|
+
var HARDCODED_PROTECTED;
|
|
9073
|
+
var init_branch_detect = __esm({
|
|
9074
|
+
"src/branch-detect.ts"() {
|
|
9075
|
+
"use strict";
|
|
9076
|
+
HARDCODED_PROTECTED = ["main", "master", "develop", "trunk"];
|
|
8874
9077
|
}
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
9078
|
+
});
|
|
9079
|
+
|
|
9080
|
+
// src/protected-branch-guard.ts
|
|
9081
|
+
function shouldRunGuard(opts) {
|
|
9082
|
+
if (opts.allowProtected) return false;
|
|
9083
|
+
if (opts.hookMode) return false;
|
|
9084
|
+
if (!opts.isTTY) return false;
|
|
9085
|
+
return true;
|
|
9086
|
+
}
|
|
9087
|
+
function detectProtectedBranchState(opts) {
|
|
9088
|
+
const branch = getCurrentBranch();
|
|
9089
|
+
const protectedList = resolveProtectedBranches({
|
|
9090
|
+
configList: opts.protectedBranches,
|
|
9091
|
+
detectDefault: opts.detectDefault !== false,
|
|
9092
|
+
defaultBranch: getDefaultBranch()
|
|
9093
|
+
});
|
|
9094
|
+
const protectedBranch = isProtectedBranch(branch, protectedList);
|
|
9095
|
+
if (!protectedBranch) {
|
|
9096
|
+
return { isProtected: false, branch, commitsAhead: 0, mode: "none" };
|
|
8878
9097
|
}
|
|
8879
|
-
|
|
9098
|
+
const commitsAhead = getCommitsAheadOfUpstream(branch);
|
|
9099
|
+
const mode = commitsAhead > 0 ? "rescue" : "uncommitted";
|
|
9100
|
+
return { isProtected: true, branch, commitsAhead, mode };
|
|
8880
9101
|
}
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
"commitlint.config.js",
|
|
8887
|
-
"commitlint.config.cjs",
|
|
8888
|
-
"commitlint.config.mjs"
|
|
8889
|
-
];
|
|
8890
|
-
for (const file of files) {
|
|
8891
|
-
const path = (0, import_path9.join)(cwd, file);
|
|
8892
|
-
if (!(0, import_fs9.existsSync)(path)) continue;
|
|
8893
|
-
try {
|
|
8894
|
-
const content = (0, import_fs9.readFileSync)(path, "utf-8");
|
|
8895
|
-
let parsed;
|
|
8896
|
-
if (file.endsWith(".json") || file === ".commitlintrc") {
|
|
8897
|
-
parsed = JSON.parse(content);
|
|
8898
|
-
} else {
|
|
8899
|
-
continue;
|
|
8900
|
-
}
|
|
8901
|
-
const rules = mapCommitlintToRules(parsed);
|
|
8902
|
-
if (rules) return rules;
|
|
8903
|
-
} catch {
|
|
8904
|
-
}
|
|
9102
|
+
var init_protected_branch_guard = __esm({
|
|
9103
|
+
"src/protected-branch-guard.ts"() {
|
|
9104
|
+
"use strict";
|
|
9105
|
+
init_branch_detect();
|
|
9106
|
+
init_git();
|
|
8905
9107
|
}
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
9108
|
+
});
|
|
9109
|
+
|
|
9110
|
+
// src/branch-name.ts
|
|
9111
|
+
function sanitizeBranchName(input) {
|
|
9112
|
+
if (!input) return null;
|
|
9113
|
+
let s = input.toLowerCase().trim();
|
|
9114
|
+
s = s.replace(/[\s_]+/g, "-");
|
|
9115
|
+
s = s.replace(/[^a-z0-9/-]/g, "");
|
|
9116
|
+
s = s.replace(/-+/g, "-").replace(/\/+/g, "/");
|
|
9117
|
+
s = s.replace(/^[-/]+|[-/]+$/g, "");
|
|
9118
|
+
if (!s.includes("/")) return null;
|
|
9119
|
+
if (s.length > MAX_BRANCH_NAME_LENGTH) {
|
|
9120
|
+
const parts = s.split("/");
|
|
9121
|
+
const type = parts[0] ?? "";
|
|
9122
|
+
const slugBudget = Math.min(MAX_BRANCH_NAME_LENGTH - type.length - 1, 52);
|
|
9123
|
+
if (slugBudget < 2) return null;
|
|
9124
|
+
s = `${type}/${parts.slice(1).join("/").slice(0, slugBudget).replace(/-+$/g, "")}`;
|
|
9125
|
+
}
|
|
9126
|
+
return validateBranchName(s) ? s : null;
|
|
9127
|
+
}
|
|
9128
|
+
function ensureUniqueName(name, exists) {
|
|
9129
|
+
if (!exists(name)) return name;
|
|
9130
|
+
for (let i = 2; i <= 100; i++) {
|
|
9131
|
+
const candidate = `${name}-${i}`;
|
|
9132
|
+
if (!exists(candidate)) return candidate;
|
|
9133
|
+
}
|
|
9134
|
+
throw new Error(`Could not find a unique name for ${name} after 100 attempts`);
|
|
9135
|
+
}
|
|
9136
|
+
function finalizeBranchName(raw, exists, options = {}) {
|
|
9137
|
+
let candidate = raw;
|
|
9138
|
+
if (!validateBranchName(candidate)) {
|
|
9139
|
+
const s = sanitizeBranchName(candidate);
|
|
9140
|
+
if (!s) {
|
|
9141
|
+
throw new Error(`Generated invalid branch name and could not sanitize: ${raw}`);
|
|
8916
9142
|
}
|
|
9143
|
+
candidate = s;
|
|
8917
9144
|
}
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
return config2.rules;
|
|
9145
|
+
if (options.skipUniqueness) {
|
|
9146
|
+
return candidate;
|
|
8921
9147
|
}
|
|
8922
|
-
return
|
|
9148
|
+
return ensureUniqueName(candidate, exists);
|
|
8923
9149
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
9150
|
+
var init_branch_name = __esm({
|
|
9151
|
+
"src/branch-name.ts"() {
|
|
9152
|
+
"use strict";
|
|
9153
|
+
init_dist();
|
|
9154
|
+
}
|
|
9155
|
+
});
|
|
9156
|
+
|
|
9157
|
+
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
9158
|
+
var require_picocolors = __commonJS({
|
|
9159
|
+
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports2, module2) {
|
|
9160
|
+
var p = process || {};
|
|
9161
|
+
var argv = p.argv || [];
|
|
9162
|
+
var env = p.env || {};
|
|
9163
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
9164
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
9165
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
9166
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
9167
|
+
};
|
|
9168
|
+
var replaceClose = (string, close, replace, index) => {
|
|
9169
|
+
let result = "", cursor = 0;
|
|
9170
|
+
do {
|
|
9171
|
+
result += string.substring(cursor, index) + replace;
|
|
9172
|
+
cursor = index + close.length;
|
|
9173
|
+
index = string.indexOf(close, cursor);
|
|
9174
|
+
} while (~index);
|
|
9175
|
+
return result + string.substring(cursor);
|
|
9176
|
+
};
|
|
9177
|
+
var createColors = (enabled = isColorSupported) => {
|
|
9178
|
+
let f = enabled ? formatter : () => String;
|
|
9179
|
+
return {
|
|
9180
|
+
isColorSupported: enabled,
|
|
9181
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
9182
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
9183
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
9184
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
9185
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
9186
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
9187
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
9188
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
9189
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
9190
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
9191
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
9192
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
9193
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
9194
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
9195
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
9196
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
9197
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
9198
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
9199
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
9200
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
9201
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
9202
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
9203
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
9204
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
9205
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
9206
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
9207
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
9208
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
9209
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
9210
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
9211
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
9212
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
9213
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
9214
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
9215
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
9216
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
9217
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
9218
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
9219
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
9220
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
9221
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
9222
|
+
};
|
|
9223
|
+
};
|
|
9224
|
+
module2.exports = createColors();
|
|
9225
|
+
module2.exports.createColors = createColors;
|
|
9226
|
+
}
|
|
9227
|
+
});
|
|
9228
|
+
|
|
9229
|
+
// src/ui.ts
|
|
9230
|
+
function hasCliNoColor() {
|
|
9231
|
+
try {
|
|
9232
|
+
return process.argv.slice(2).includes("--no-color");
|
|
9233
|
+
} catch {
|
|
9234
|
+
return false;
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
function getTerminalWidth() {
|
|
9238
|
+
return process.stderr.columns ?? process.stdout.columns ?? 80;
|
|
9239
|
+
}
|
|
9240
|
+
function createUI(options) {
|
|
9241
|
+
const isColor = options.isTTY && !options.noColor;
|
|
9242
|
+
const wrap = (fn) => (s) => isColor ? fn(s) : s;
|
|
9243
|
+
const format = {
|
|
9244
|
+
step: (msg) => `${isColor ? import_picocolors.default.dim("\u203A") : "\u203A"} ${isColor ? import_picocolors.default.dim(msg) : msg}`,
|
|
9245
|
+
success: (msg) => `${isColor ? import_picocolors.default.green("\u2713") : "\u2713"} ${msg}`,
|
|
9246
|
+
error: (msg) => `${isColor ? import_picocolors.default.red("\u2717") : "\u2717"} ${msg}`,
|
|
9247
|
+
dim: wrap(import_picocolors.default.dim),
|
|
9248
|
+
bold: wrap(import_picocolors.default.bold),
|
|
9249
|
+
commitType: wrap(import_picocolors.default.cyan),
|
|
9250
|
+
commitScope: wrap(import_picocolors.default.yellow),
|
|
9251
|
+
accent: wrap(import_picocolors.default.magenta)
|
|
9252
|
+
};
|
|
9253
|
+
function createSpinner(message, write = (s) => process.stderr.write(s)) {
|
|
9254
|
+
let frame = 0;
|
|
9255
|
+
let interval = null;
|
|
9256
|
+
return {
|
|
9257
|
+
start() {
|
|
9258
|
+
if (interval) return;
|
|
9259
|
+
if (!options.isTTY) return;
|
|
9260
|
+
interval = setInterval(() => {
|
|
9261
|
+
const f = SPINNER_FRAMES2[frame++ % SPINNER_FRAMES2.length];
|
|
9262
|
+
write(`\r${format.step(message)} ${isColor ? import_picocolors.default.cyan(f) : f}`);
|
|
9263
|
+
}, 80);
|
|
9264
|
+
},
|
|
9265
|
+
stop(finalMessage) {
|
|
9266
|
+
if (interval) {
|
|
9267
|
+
clearInterval(interval);
|
|
9268
|
+
interval = null;
|
|
9269
|
+
}
|
|
9270
|
+
if (options.isTTY) {
|
|
9271
|
+
write("\r\x1B[2K");
|
|
9272
|
+
}
|
|
9273
|
+
if (finalMessage) {
|
|
9274
|
+
write(finalMessage + "\n");
|
|
9275
|
+
}
|
|
9276
|
+
}
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
9279
|
+
const log = {
|
|
9280
|
+
step: (msg) => process.stderr.write(format.step(msg) + "\n"),
|
|
9281
|
+
success: (msg) => process.stderr.write(format.success(msg) + "\n"),
|
|
9282
|
+
error: (msg) => process.stderr.write(format.error(msg) + "\n"),
|
|
9283
|
+
dim: (msg) => process.stderr.write(format.dim(msg) + "\n")
|
|
9284
|
+
};
|
|
9285
|
+
return { isColor, format, spinner: createSpinner, log };
|
|
9286
|
+
}
|
|
9287
|
+
function getUI() {
|
|
9288
|
+
if (!_defaultUI) {
|
|
9289
|
+
_defaultUI = createUI({
|
|
9290
|
+
isTTY: !!process.stderr.isTTY,
|
|
9291
|
+
noColor: !!process.env.NO_COLOR || hasCliNoColor()
|
|
9292
|
+
});
|
|
9293
|
+
}
|
|
9294
|
+
return _defaultUI;
|
|
9295
|
+
}
|
|
9296
|
+
var import_picocolors, SPINNER_FRAMES2, _defaultUI, ui;
|
|
9297
|
+
var init_ui = __esm({
|
|
9298
|
+
"src/ui.ts"() {
|
|
9299
|
+
"use strict";
|
|
9300
|
+
import_picocolors = __toESM(require_picocolors());
|
|
9301
|
+
SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
9302
|
+
ui = new Proxy({}, {
|
|
9303
|
+
get(_target, prop) {
|
|
9304
|
+
return getUI()[prop];
|
|
9305
|
+
}
|
|
9306
|
+
});
|
|
9307
|
+
}
|
|
9308
|
+
});
|
|
9309
|
+
|
|
9310
|
+
// src/ui-rich.ts
|
|
9311
|
+
function splitCommitForBox(message) {
|
|
9312
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
9313
|
+
const m = firstLine.match(HEADER_RX);
|
|
9314
|
+
if (!m) return { type: null, scope: null, subject: firstLine, breaking: false };
|
|
9315
|
+
return {
|
|
9316
|
+
type: m[1] ?? null,
|
|
9317
|
+
scope: m[2] ?? null,
|
|
9318
|
+
breaking: m[3] === "!",
|
|
9319
|
+
subject: m[4] ?? ""
|
|
9320
|
+
};
|
|
9321
|
+
}
|
|
9322
|
+
function renderFileTree(files, maxFiles) {
|
|
9323
|
+
if (files.length === 0) return "";
|
|
9324
|
+
const lines = [];
|
|
9325
|
+
const display = files.slice(0, maxFiles);
|
|
9326
|
+
const overflow = Math.max(0, files.length - maxFiles);
|
|
9327
|
+
for (let i = 0; i < display.length; i++) {
|
|
9328
|
+
const isLast = i === display.length - 1 && overflow === 0;
|
|
9329
|
+
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
9330
|
+
lines.push(` ${connector} ${display[i]}`);
|
|
9331
|
+
}
|
|
9332
|
+
if (overflow > 0) {
|
|
9333
|
+
lines.push(` \u2514\u2500 +${overflow} more files`);
|
|
9334
|
+
}
|
|
9335
|
+
return lines.join("\n");
|
|
9336
|
+
}
|
|
9337
|
+
function wrapLine(text, width) {
|
|
9338
|
+
if (text.length <= width) return [text];
|
|
9339
|
+
const result = [];
|
|
9340
|
+
let remaining = text;
|
|
9341
|
+
while (remaining.length > width) {
|
|
9342
|
+
let breakAt = remaining.lastIndexOf(" ", width);
|
|
9343
|
+
if (breakAt < width / 2) breakAt = width;
|
|
9344
|
+
result.push(remaining.slice(0, breakAt));
|
|
9345
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
9346
|
+
}
|
|
9347
|
+
if (remaining) result.push(remaining);
|
|
9348
|
+
return result;
|
|
9349
|
+
}
|
|
9350
|
+
function stripAnsi(s) {
|
|
9351
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
9352
|
+
}
|
|
9353
|
+
function boxedLine(content, innerWidth, isColor) {
|
|
9354
|
+
const visibleLen = stripAnsi(content).length;
|
|
9355
|
+
const padding = " ".repeat(Math.max(0, innerWidth - visibleLen));
|
|
9356
|
+
const border = isColor ? import_picocolors2.default.dim("\u2502") : "\u2502";
|
|
9357
|
+
return ` ${border} ${content}${padding} ${border}`;
|
|
9358
|
+
}
|
|
9359
|
+
function renderBoxedCommit(header, body, opts) {
|
|
9360
|
+
if (opts.width < MIN_BOX_WIDTH) {
|
|
9361
|
+
const lines2 = [header.split("\n")[0] ?? header];
|
|
9362
|
+
if (body) lines2.push("", body);
|
|
9363
|
+
return lines2.join("\n");
|
|
9364
|
+
}
|
|
9365
|
+
const innerWidth = opts.width - 8;
|
|
9366
|
+
const horiz = opts.width - 2;
|
|
9367
|
+
const top = " " + (opts.isColor ? import_picocolors2.default.dim("\u256D" + "\u2500".repeat(horiz) + "\u256E") : "\u256D" + "\u2500".repeat(horiz) + "\u256E");
|
|
9368
|
+
const bottom = " " + (opts.isColor ? import_picocolors2.default.dim("\u2570" + "\u2500".repeat(horiz) + "\u256F") : "\u2570" + "\u2500".repeat(horiz) + "\u256F");
|
|
9369
|
+
const parsed = splitCommitForBox(header);
|
|
9370
|
+
let firstLineStyled;
|
|
9371
|
+
if (parsed.type && parsed.scope) {
|
|
9372
|
+
const bare = `${parsed.type}(${parsed.scope})${parsed.breaking ? "!" : ""}: ${parsed.subject}`;
|
|
9373
|
+
firstLineStyled = opts.isColor ? `${import_picocolors2.default.bold(import_picocolors2.default.cyan(parsed.type))}(${import_picocolors2.default.bold(import_picocolors2.default.yellow(parsed.scope))})${parsed.breaking ? import_picocolors2.default.bold(import_picocolors2.default.red("!")) : ""}: ${parsed.subject}` : bare;
|
|
9374
|
+
} else if (parsed.type) {
|
|
9375
|
+
const bare = `${parsed.type}${parsed.breaking ? "!" : ""}: ${parsed.subject}`;
|
|
9376
|
+
firstLineStyled = opts.isColor ? `${import_picocolors2.default.bold(import_picocolors2.default.cyan(parsed.type))}${parsed.breaking ? import_picocolors2.default.bold(import_picocolors2.default.red("!")) : ""}: ${parsed.subject}` : bare;
|
|
9377
|
+
} else {
|
|
9378
|
+
firstLineStyled = header.split("\n")[0] ?? header;
|
|
9379
|
+
}
|
|
9380
|
+
const lines = [];
|
|
9381
|
+
const headerParts = wrapLine(firstLineStyled, innerWidth);
|
|
9382
|
+
for (let i = 0; i < headerParts.length; i++) {
|
|
9383
|
+
const indent = i === 0 ? "" : " ";
|
|
9384
|
+
lines.push(boxedLine(indent + (headerParts[i] ?? ""), innerWidth, opts.isColor));
|
|
9385
|
+
}
|
|
9386
|
+
if (body) {
|
|
9387
|
+
lines.push(boxedLine("", innerWidth, opts.isColor));
|
|
9388
|
+
for (const bline of body.split("\n")) {
|
|
9389
|
+
const trimmed = bline.trim();
|
|
9390
|
+
if (!trimmed) continue;
|
|
9391
|
+
const rendered = trimmed.replace(/^[-*]\s+/, opts.isColor ? `${import_picocolors2.default.green("\u2022")} ` : "\u2022 ");
|
|
9392
|
+
const wrapped = wrapLine(rendered, innerWidth);
|
|
9393
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
9394
|
+
lines.push(boxedLine((i === 0 ? "" : " ") + (wrapped[i] ?? ""), innerWidth, opts.isColor));
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
return [top, ...lines, bottom].join("\n");
|
|
9399
|
+
}
|
|
9400
|
+
function renderStatsLine(stats, isColor) {
|
|
9401
|
+
const parts = [];
|
|
9402
|
+
parts.push(`${stats.files} files`);
|
|
9403
|
+
parts.push(`+${stats.additions} \u2212${stats.deletions}`);
|
|
9404
|
+
if (stats.tokens !== void 0) parts.push(`${stats.tokens} tokens`);
|
|
9405
|
+
const text = parts.join(" \xB7 ");
|
|
9406
|
+
return isColor ? ` ${import_picocolors2.default.dim(text)}` : ` ${text}`;
|
|
9407
|
+
}
|
|
9408
|
+
function shouldUseRichOutput(opts) {
|
|
9409
|
+
if (!opts.isTTY) return false;
|
|
9410
|
+
if (opts.noColor) return false;
|
|
9411
|
+
if (opts.style !== "rich") return false;
|
|
9412
|
+
if (opts.width < MIN_BOX_WIDTH) return false;
|
|
9413
|
+
return true;
|
|
9414
|
+
}
|
|
9415
|
+
var import_picocolors2, HEADER_RX, MIN_BOX_WIDTH;
|
|
9416
|
+
var init_ui_rich = __esm({
|
|
9417
|
+
"src/ui-rich.ts"() {
|
|
9418
|
+
"use strict";
|
|
9419
|
+
import_picocolors2 = __toESM(require_picocolors());
|
|
9420
|
+
init_ui();
|
|
9421
|
+
HEADER_RX = /^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*(.*)$/;
|
|
9422
|
+
MIN_BOX_WIDTH = 60;
|
|
9423
|
+
}
|
|
9424
|
+
});
|
|
9425
|
+
|
|
9426
|
+
// src/commit-helpers.ts
|
|
9427
|
+
function applyCliTypeScopeToRules(rules, type, scope) {
|
|
9428
|
+
let next = { ...rules };
|
|
9429
|
+
if (type) {
|
|
9430
|
+
next = { ...next, types: [type] };
|
|
9431
|
+
}
|
|
9432
|
+
if (scope) {
|
|
9433
|
+
next = { ...next, scopes: [scope] };
|
|
9434
|
+
}
|
|
9435
|
+
return next;
|
|
9436
|
+
}
|
|
9437
|
+
function generationHintsFromArgs(split, forceBody) {
|
|
9438
|
+
const h = {};
|
|
9439
|
+
if (split) h.split = true;
|
|
9440
|
+
if (forceBody) h.force_body = true;
|
|
9441
|
+
return Object.keys(h).length > 0 ? h : void 0;
|
|
9442
|
+
}
|
|
9443
|
+
function splitCommitMessageForDisplay(message) {
|
|
9444
|
+
const t = message.replace(/\r\n/g, "\n").trimEnd();
|
|
9445
|
+
const doubleNl = t.indexOf("\n\n");
|
|
9446
|
+
if (doubleNl !== -1) {
|
|
9447
|
+
const head = t.slice(0, doubleNl);
|
|
9448
|
+
const subject = head.split("\n")[0]?.trim() ?? "";
|
|
9449
|
+
return { subject, body: t.slice(doubleNl + 2).trimEnd() };
|
|
9450
|
+
}
|
|
9451
|
+
const firstNl = t.indexOf("\n");
|
|
9452
|
+
if (firstNl === -1) {
|
|
9453
|
+
return { subject: t.trim(), body: "" };
|
|
9454
|
+
}
|
|
9455
|
+
return {
|
|
9456
|
+
subject: t.slice(0, firstNl).trim(),
|
|
9457
|
+
body: t.slice(firstNl + 1).trimEnd()
|
|
9458
|
+
};
|
|
9459
|
+
}
|
|
9460
|
+
function formatVerboseCommitDiagnostics(diagnostics, roundTripMs) {
|
|
9461
|
+
const lines = [`api_round_trip_ms: ${roundTripMs}`];
|
|
9462
|
+
if (diagnostics !== void 0) {
|
|
9463
|
+
lines.push(JSON.stringify(diagnostics, null, 2));
|
|
9464
|
+
}
|
|
9465
|
+
return lines.join("\n");
|
|
9466
|
+
}
|
|
9467
|
+
async function interactiveRefineMessage(initial, opts) {
|
|
9468
|
+
if (opts.skip) return { action: "accept", message: initial };
|
|
9469
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9470
|
+
try {
|
|
9471
|
+
process.stderr.write(`
|
|
9472
|
+
${initial}
|
|
9473
|
+
|
|
9474
|
+
`);
|
|
9475
|
+
const choice = (await rl.question("Keep? [Y/n/e]: ")).trim().toLowerCase();
|
|
9476
|
+
if (choice === "n") {
|
|
9477
|
+
return { action: "abort" };
|
|
9478
|
+
}
|
|
9479
|
+
if (choice === "e") {
|
|
9480
|
+
process.stderr.write("Enter new message (end with a line containing only .):\n");
|
|
9481
|
+
const lines = [];
|
|
9482
|
+
while (true) {
|
|
9483
|
+
const line = await rl.question("");
|
|
9484
|
+
if (line === ".") break;
|
|
9485
|
+
lines.push(line);
|
|
9486
|
+
}
|
|
9487
|
+
const edited = lines.join("\n").trim();
|
|
9488
|
+
return { action: "edit", message: edited.length > 0 ? edited : initial };
|
|
9489
|
+
}
|
|
9490
|
+
return { action: "accept", message: initial };
|
|
9491
|
+
} finally {
|
|
9492
|
+
rl.close();
|
|
9493
|
+
}
|
|
9494
|
+
}
|
|
9495
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
9496
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9497
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
9498
|
+
try {
|
|
9499
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
9500
|
+
if (answer === "n" || answer === "no") return false;
|
|
9501
|
+
if (answer === "y" || answer === "yes") return true;
|
|
9502
|
+
return defaultYes;
|
|
9503
|
+
} finally {
|
|
9504
|
+
rl.close();
|
|
9505
|
+
}
|
|
9506
|
+
}
|
|
9507
|
+
async function confirmCommit(prompt2, opts) {
|
|
9508
|
+
if (opts.skip) return { action: "commit" };
|
|
9509
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9510
|
+
try {
|
|
9511
|
+
const ans = (await rl.question(prompt2)).trim().toLowerCase();
|
|
9512
|
+
if (ans !== "y" && ans !== "yes") {
|
|
9513
|
+
return { action: "abort" };
|
|
9514
|
+
}
|
|
9515
|
+
return { action: "commit" };
|
|
9516
|
+
} finally {
|
|
9517
|
+
rl.close();
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
function shouldSkipTTYInteraction(hookMode) {
|
|
9521
|
+
return hookMode === true || process.stdin.isTTY !== true;
|
|
9522
|
+
}
|
|
9523
|
+
function logVerboseDiagnostics(dim, verbose, quiet, diagnostics, roundTripMs) {
|
|
9524
|
+
if (!verbose || quiet) return;
|
|
9525
|
+
process.stderr.write(
|
|
9526
|
+
`
|
|
9527
|
+
${formatVerboseCommitDiagnostics(diagnostics, roundTripMs)}
|
|
9528
|
+
`
|
|
9529
|
+
);
|
|
9530
|
+
dim("(verbose diagnostics on stderr)");
|
|
9531
|
+
}
|
|
9532
|
+
function isDisplayOpts(opts) {
|
|
9533
|
+
return typeof opts === "object" && opts !== null && "log" in opts;
|
|
9534
|
+
}
|
|
9535
|
+
function createSilentLog() {
|
|
9536
|
+
return {
|
|
9537
|
+
step: () => {
|
|
9538
|
+
},
|
|
9539
|
+
success: () => {
|
|
9540
|
+
},
|
|
9541
|
+
error: (msg) => console.error(msg),
|
|
9542
|
+
dim: () => {
|
|
9543
|
+
}
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
function displayCommitMessage(message, opts) {
|
|
9547
|
+
const display = isDisplayOpts(opts) ? opts : { log: opts };
|
|
9548
|
+
const log = display.log;
|
|
9549
|
+
const { subject, body } = splitCommitMessageForDisplay(message);
|
|
9550
|
+
const tw = getTerminalWidth();
|
|
9551
|
+
const useRich = shouldUseRichOutput({
|
|
9552
|
+
isTTY: display.isTTY ?? !!process.stderr.isTTY,
|
|
9553
|
+
noColor: display.isColor === false,
|
|
9554
|
+
width: tw,
|
|
9555
|
+
style: display.style ?? "rich"
|
|
9556
|
+
});
|
|
9557
|
+
if (useRich) {
|
|
9558
|
+
const tree = display.stagedFiles && display.stagedFiles.length > 0 ? renderFileTree(display.stagedFiles, 8) : "";
|
|
9559
|
+
if (tree) {
|
|
9560
|
+
process.stderr.write(tree + "\n");
|
|
9561
|
+
}
|
|
9562
|
+
const boxed = renderBoxedCommit(subject, body, {
|
|
9563
|
+
width: Math.min(Math.max(tw - 4, 60), 80),
|
|
9564
|
+
isColor: !!display.isColor
|
|
9565
|
+
});
|
|
9566
|
+
process.stderr.write(boxed + "\n");
|
|
9567
|
+
if (display.stats) {
|
|
9568
|
+
process.stderr.write(renderStatsLine(display.stats, !!display.isColor) + "\n");
|
|
9569
|
+
}
|
|
9570
|
+
return;
|
|
9571
|
+
}
|
|
9572
|
+
log.success(subject);
|
|
9573
|
+
if (body) {
|
|
9574
|
+
for (const line of body.split("\n")) {
|
|
9575
|
+
log.dim(` ${line}`);
|
|
9576
|
+
}
|
|
9577
|
+
process.stderr.write("\n");
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
var import_promises;
|
|
9581
|
+
var init_commit_helpers = __esm({
|
|
9582
|
+
"src/commit-helpers.ts"() {
|
|
9583
|
+
"use strict";
|
|
9584
|
+
import_promises = __toESM(require("node:readline/promises"));
|
|
9585
|
+
init_ui_rich();
|
|
9586
|
+
}
|
|
9587
|
+
});
|
|
9588
|
+
|
|
9589
|
+
// src/smart-diff.ts
|
|
9590
|
+
function sanitizeFilepath(path) {
|
|
9591
|
+
return path.replace(/[\x00-\x1F\x7F[\]`]/g, "_").slice(0, 200);
|
|
9592
|
+
}
|
|
9593
|
+
function classifyFile(filepath) {
|
|
9594
|
+
const basename = filepath.split("/").pop() ?? filepath;
|
|
9595
|
+
if (LOCK_FILES.has(basename)) return "lock";
|
|
9596
|
+
if (filepath.endsWith(".map")) return "sourcemap";
|
|
9597
|
+
if (VENDORED_PREFIXES.some((p) => filepath.startsWith(p))) return "vendored";
|
|
9598
|
+
if (GENERATED_PATTERNS.some((p) => p.test(filepath))) return "generated";
|
|
9599
|
+
return "code";
|
|
9600
|
+
}
|
|
9601
|
+
function parseDiffIntoFiles(diff) {
|
|
9602
|
+
const files = [];
|
|
9603
|
+
const parts = diff.split(/^(diff --git .+)$/m);
|
|
9604
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
9605
|
+
const header = parts[i];
|
|
9606
|
+
const content = parts[i + 1] ?? "";
|
|
9607
|
+
const match = header.match(/diff --git a\/(.+?) b\/(.+)/);
|
|
9608
|
+
const filepath = match?.[2] ?? "unknown";
|
|
9609
|
+
const lines = content.split("\n");
|
|
9610
|
+
let additions = 0;
|
|
9611
|
+
let deletions = 0;
|
|
9612
|
+
for (const line of lines) {
|
|
9613
|
+
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
9614
|
+
else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
|
|
9615
|
+
}
|
|
9616
|
+
files.push({ filepath, content: header + content, additions, deletions });
|
|
9617
|
+
}
|
|
9618
|
+
return files;
|
|
9619
|
+
}
|
|
9620
|
+
function isMinified(content) {
|
|
9621
|
+
const lines = content.split("\n").filter(
|
|
9622
|
+
(l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---")
|
|
9623
|
+
);
|
|
9624
|
+
if (lines.length === 0) return false;
|
|
9625
|
+
return lines.some((l) => l.length > 500);
|
|
9626
|
+
}
|
|
9627
|
+
function buildFileSummary(file) {
|
|
9628
|
+
const sizeKB = Math.round(file.content.length / 1024);
|
|
9629
|
+
return `[modified: ${sanitizeFilepath(file.filepath)} \u2014 +${file.additions} \u2212${file.deletions} lines, ~${sizeKB}KB]
|
|
9630
|
+
`;
|
|
9631
|
+
}
|
|
9632
|
+
function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
|
|
9633
|
+
const files = parseDiffIntoFiles(diff);
|
|
9634
|
+
if (files.length === 0) {
|
|
9635
|
+
return { processedDiff: diff, summarized: [], aggressivelySummarized: [], tokensSaved: 0 };
|
|
9636
|
+
}
|
|
9637
|
+
const entries = [];
|
|
9638
|
+
const summarized = [];
|
|
9639
|
+
let tokensSaved = 0;
|
|
9640
|
+
for (const file of files) {
|
|
9641
|
+
const classification = classifyFile(file.filepath);
|
|
9642
|
+
switch (classification) {
|
|
9643
|
+
case "sourcemap":
|
|
9644
|
+
tokensSaved += estimateTokens(file.content);
|
|
9645
|
+
summarized.push(file.filepath);
|
|
9646
|
+
entries.push({ file, isNoise: true, summaryLine: null });
|
|
9647
|
+
break;
|
|
9648
|
+
case "lock":
|
|
9649
|
+
tokensSaved += estimateTokens(file.content);
|
|
9650
|
+
summarized.push(file.filepath);
|
|
9651
|
+
entries.push({
|
|
9652
|
+
file,
|
|
9653
|
+
isNoise: true,
|
|
9654
|
+
summaryLine: `[lock file updated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions} lines)]
|
|
9655
|
+
`
|
|
9656
|
+
});
|
|
9657
|
+
break;
|
|
9658
|
+
case "generated":
|
|
9659
|
+
tokensSaved += estimateTokens(file.content);
|
|
9660
|
+
summarized.push(file.filepath);
|
|
9661
|
+
entries.push({
|
|
9662
|
+
file,
|
|
9663
|
+
isNoise: true,
|
|
9664
|
+
summaryLine: `[generated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions})]
|
|
9665
|
+
`
|
|
9666
|
+
});
|
|
9667
|
+
break;
|
|
9668
|
+
case "vendored":
|
|
9669
|
+
tokensSaved += estimateTokens(file.content);
|
|
9670
|
+
summarized.push(file.filepath);
|
|
9671
|
+
entries.push({
|
|
9672
|
+
file,
|
|
9673
|
+
isNoise: true,
|
|
9674
|
+
summaryLine: `[vendored: ${sanitizeFilepath(file.filepath)} updated]
|
|
9675
|
+
`
|
|
9676
|
+
});
|
|
9677
|
+
break;
|
|
9678
|
+
case "code":
|
|
9679
|
+
if (isMinified(file.content)) {
|
|
9680
|
+
tokensSaved += estimateTokens(file.content);
|
|
9681
|
+
const sizeKB = Math.round(file.content.length / 1024);
|
|
9682
|
+
summarized.push(file.filepath);
|
|
9683
|
+
entries.push({
|
|
9684
|
+
file,
|
|
9685
|
+
isNoise: true,
|
|
9686
|
+
summaryLine: `[minified asset: ${sanitizeFilepath(file.filepath)} (${sizeKB} KB)]
|
|
9687
|
+
`
|
|
9688
|
+
});
|
|
9689
|
+
} else {
|
|
9690
|
+
entries.push({ file, isNoise: false, summaryLine: null });
|
|
9691
|
+
}
|
|
9692
|
+
break;
|
|
9693
|
+
}
|
|
9694
|
+
}
|
|
9695
|
+
const aggressiveMap = /* @__PURE__ */ new Map();
|
|
9696
|
+
function buildOutput() {
|
|
9697
|
+
const parts = [];
|
|
9698
|
+
for (const entry of entries) {
|
|
9699
|
+
if (entry.isNoise) {
|
|
9700
|
+
if (entry.summaryLine !== null) parts.push(entry.summaryLine);
|
|
9701
|
+
} else if (aggressiveMap.has(entry.file.filepath)) {
|
|
9702
|
+
parts.push(aggressiveMap.get(entry.file.filepath));
|
|
9703
|
+
} else {
|
|
9704
|
+
parts.push(entry.file.content);
|
|
9705
|
+
}
|
|
9706
|
+
}
|
|
9707
|
+
return parts.join("");
|
|
9708
|
+
}
|
|
9709
|
+
const codeEntries = entries.filter((e) => !e.isNoise);
|
|
9710
|
+
let output = buildOutput();
|
|
9711
|
+
if (output.length <= maxBytes) {
|
|
9712
|
+
return {
|
|
9713
|
+
processedDiff: output,
|
|
9714
|
+
summarized,
|
|
9715
|
+
aggressivelySummarized: [],
|
|
9716
|
+
tokensSaved
|
|
9717
|
+
};
|
|
9718
|
+
}
|
|
9719
|
+
const TIER1_THRESHOLD = 5 * 1024;
|
|
9720
|
+
for (const entry of codeEntries) {
|
|
9721
|
+
if (entry.file.content.length > TIER1_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
|
|
9722
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9723
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
output = buildOutput();
|
|
9727
|
+
if (output.length <= maxBytes) {
|
|
9728
|
+
return {
|
|
9729
|
+
processedDiff: output,
|
|
9730
|
+
summarized,
|
|
9731
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9732
|
+
tokensSaved
|
|
9733
|
+
};
|
|
9734
|
+
}
|
|
9735
|
+
const TIER2_THRESHOLD = 2 * 1024;
|
|
9736
|
+
for (const entry of codeEntries) {
|
|
9737
|
+
if (entry.file.content.length > TIER2_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
|
|
9738
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9739
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9740
|
+
}
|
|
9741
|
+
}
|
|
9742
|
+
output = buildOutput();
|
|
9743
|
+
if (output.length <= maxBytes) {
|
|
9744
|
+
return {
|
|
9745
|
+
processedDiff: output,
|
|
9746
|
+
summarized,
|
|
9747
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9748
|
+
tokensSaved
|
|
9749
|
+
};
|
|
9750
|
+
}
|
|
9751
|
+
for (const entry of codeEntries) {
|
|
9752
|
+
if (!aggressiveMap.has(entry.file.filepath)) {
|
|
9753
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9754
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9755
|
+
}
|
|
9756
|
+
}
|
|
9757
|
+
return {
|
|
9758
|
+
processedDiff: buildOutput(),
|
|
9759
|
+
summarized,
|
|
9760
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9761
|
+
tokensSaved
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
var LOCK_FILES, GENERATED_PATTERNS, VENDORED_PREFIXES;
|
|
9765
|
+
var init_smart_diff = __esm({
|
|
9766
|
+
"src/smart-diff.ts"() {
|
|
9767
|
+
"use strict";
|
|
9768
|
+
init_dist();
|
|
9769
|
+
LOCK_FILES = /* @__PURE__ */ new Set([
|
|
9770
|
+
"pnpm-lock.yaml",
|
|
9771
|
+
"package-lock.json",
|
|
9772
|
+
"yarn.lock",
|
|
9773
|
+
"Cargo.lock",
|
|
9774
|
+
"Gemfile.lock",
|
|
9775
|
+
"poetry.lock",
|
|
9776
|
+
"composer.lock",
|
|
9777
|
+
"bun.lockb",
|
|
9778
|
+
"shrinkwrap.json"
|
|
9779
|
+
]);
|
|
9780
|
+
GENERATED_PATTERNS = [
|
|
9781
|
+
/\.generated\.\w+$/,
|
|
9782
|
+
/\.g\.dart$/,
|
|
9783
|
+
/\.pb\.go$/,
|
|
9784
|
+
/\.pb\.ts$/,
|
|
9785
|
+
/(^|\/)\.prisma\/client\//,
|
|
9786
|
+
/\/generated\//
|
|
9787
|
+
];
|
|
9788
|
+
VENDORED_PREFIXES = ["vendor/", "third_party/", "node_modules/"];
|
|
9789
|
+
}
|
|
9790
|
+
});
|
|
9791
|
+
|
|
9792
|
+
// src/local.ts
|
|
9793
|
+
var local_exports = {};
|
|
9794
|
+
__export(local_exports, {
|
|
9795
|
+
generateLocalBranchName: () => generateLocalBranchName,
|
|
9796
|
+
getLocalProviderConfig: () => getLocalProviderConfig,
|
|
9797
|
+
runLocalBranch: () => runLocalBranch,
|
|
9798
|
+
runLocalCommit: () => runLocalCommit
|
|
9799
|
+
});
|
|
9800
|
+
function getLegacyProvider() {
|
|
9801
|
+
try {
|
|
9802
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "provider");
|
|
9803
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9804
|
+
const v = (0, import_fs8.readFileSync)(p, "utf-8").trim().toLowerCase();
|
|
9805
|
+
if (["ollama", "lmstudio", "openrouter", "custom", "cloudflare"].includes(v)) {
|
|
9806
|
+
return v;
|
|
8937
9807
|
}
|
|
8938
|
-
break;
|
|
8939
9808
|
}
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
9809
|
+
} catch {
|
|
9810
|
+
}
|
|
9811
|
+
return null;
|
|
9812
|
+
}
|
|
9813
|
+
function getLegacyBaseUrl(provider) {
|
|
9814
|
+
try {
|
|
9815
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "base_url");
|
|
9816
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9817
|
+
return (0, import_fs8.readFileSync)(p, "utf-8").trim();
|
|
9818
|
+
}
|
|
9819
|
+
} catch {
|
|
9820
|
+
}
|
|
9821
|
+
return PROVIDER_URLS[provider] ?? "";
|
|
9822
|
+
}
|
|
9823
|
+
function getLegacyModel(provider) {
|
|
9824
|
+
try {
|
|
9825
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "model");
|
|
9826
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9827
|
+
const v = (0, import_fs8.readFileSync)(p, "utf-8").trim();
|
|
9828
|
+
if (v) return v;
|
|
9829
|
+
}
|
|
9830
|
+
} catch {
|
|
9831
|
+
}
|
|
9832
|
+
return DEFAULT_MODELS[provider] ?? "";
|
|
9833
|
+
}
|
|
9834
|
+
function getLocalProviderConfig() {
|
|
9835
|
+
const config2 = getConfig();
|
|
9836
|
+
const provider = config2.provider ?? getLegacyProvider();
|
|
9837
|
+
if (!provider) return null;
|
|
9838
|
+
const baseUrl = config2.apiUrl ?? getLegacyBaseUrl(provider) ?? PROVIDER_URLS[provider] ?? "";
|
|
9839
|
+
if (!baseUrl) return null;
|
|
9840
|
+
const model = config2.model ?? getLegacyModel(provider) ?? DEFAULT_MODELS[provider];
|
|
9841
|
+
const apiKey = provider === "openrouter" || provider === "custom" ? getApiKey() : null;
|
|
9842
|
+
if (provider === "openrouter" && !apiKey) return null;
|
|
9843
|
+
return { provider, baseUrl, model, apiKey };
|
|
9844
|
+
}
|
|
9845
|
+
function buildUserPrompt(changes, diff, rules, recentCommits, hints) {
|
|
9846
|
+
let prompt2 = `Generate a commit message for these changes:
|
|
9847
|
+
|
|
9848
|
+
## File changes:
|
|
9849
|
+
<file_changes>
|
|
9850
|
+
${changes}
|
|
9851
|
+
</file_changes>
|
|
9852
|
+
|
|
9853
|
+
## Diff:
|
|
9854
|
+
<diff>
|
|
9855
|
+
${diff}
|
|
9856
|
+
</diff>
|
|
9857
|
+
|
|
9858
|
+
`;
|
|
9859
|
+
if (recentCommits && recentCommits.length > 0) {
|
|
9860
|
+
const history = recentCommits.slice(0, 10).join("\n");
|
|
9861
|
+
prompt2 += `Recent commits on this branch (match style when appropriate):
|
|
9862
|
+
${history}
|
|
9863
|
+
|
|
9864
|
+
`;
|
|
9865
|
+
}
|
|
9866
|
+
if (hints?.split) {
|
|
9867
|
+
prompt2 += `MULTI-COMMIT MODE: If changes span multiple logical commits, focus the message on the primary change and mention other slices in the body.
|
|
9868
|
+
|
|
9869
|
+
`;
|
|
9870
|
+
}
|
|
9871
|
+
if (hints?.force_body) {
|
|
9872
|
+
prompt2 += `The user requires a BODY section after the subject line, even for small changes.
|
|
9873
|
+
|
|
9874
|
+
`;
|
|
9875
|
+
}
|
|
9876
|
+
if (rules && Object.keys(rules).length > 0) {
|
|
9877
|
+
prompt2 += `Rules: ${JSON.stringify(rules)}
|
|
9878
|
+
|
|
9879
|
+
`;
|
|
9880
|
+
}
|
|
9881
|
+
prompt2 += `Important:
|
|
9882
|
+
- Follow conventional commit format: <type>(<scope>): <subject>
|
|
9883
|
+
- Response should be the commit message only, no explanations`;
|
|
9884
|
+
return prompt2;
|
|
9885
|
+
}
|
|
9886
|
+
function buildRequest(provider, baseUrl, userContent, diff, changes, model, apiKey, rules, recentCommits, hints) {
|
|
9887
|
+
const headers = {
|
|
9888
|
+
"Content-Type": "application/json"
|
|
9889
|
+
};
|
|
9890
|
+
if (apiKey) {
|
|
9891
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
9892
|
+
}
|
|
9893
|
+
if (provider === "openrouter") {
|
|
9894
|
+
headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
|
|
9895
|
+
headers["X-Title"] = "qc - AI Commit Message Generator";
|
|
9896
|
+
}
|
|
9897
|
+
let url;
|
|
9898
|
+
let body;
|
|
9899
|
+
switch (provider) {
|
|
9900
|
+
case "ollama":
|
|
9901
|
+
url = `${baseUrl}/api/generate`;
|
|
9902
|
+
body = {
|
|
9903
|
+
model,
|
|
9904
|
+
prompt: userContent,
|
|
9905
|
+
stream: false,
|
|
9906
|
+
options: {}
|
|
9907
|
+
};
|
|
9908
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9909
|
+
case "lmstudio":
|
|
9910
|
+
url = `${baseUrl}/chat/completions`;
|
|
9911
|
+
body = {
|
|
9912
|
+
model,
|
|
9913
|
+
stream: false,
|
|
9914
|
+
messages: [
|
|
9915
|
+
{
|
|
9916
|
+
role: "system",
|
|
9917
|
+
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9918
|
+
},
|
|
9919
|
+
{ role: "user", content: userContent }
|
|
9920
|
+
]
|
|
9921
|
+
};
|
|
9922
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9923
|
+
case "openrouter":
|
|
9924
|
+
case "custom":
|
|
9925
|
+
url = `${baseUrl}/chat/completions`;
|
|
9926
|
+
body = {
|
|
9927
|
+
model,
|
|
9928
|
+
stream: false,
|
|
9929
|
+
messages: [
|
|
9930
|
+
{
|
|
9931
|
+
role: "system",
|
|
9932
|
+
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9933
|
+
},
|
|
9934
|
+
{ role: "user", content: userContent }
|
|
9935
|
+
]
|
|
9936
|
+
};
|
|
9937
|
+
return { url, body, headers };
|
|
9938
|
+
case "cloudflare": {
|
|
9939
|
+
url = `${baseUrl.replace(/\/$/, "")}/commit`;
|
|
9940
|
+
const payload = { diff, changes, rules };
|
|
9941
|
+
if (recentCommits && recentCommits.length > 0) {
|
|
9942
|
+
payload.recent_commits = recentCommits.slice(0, 10);
|
|
8953
9943
|
}
|
|
8954
|
-
|
|
9944
|
+
if (hints && Object.keys(hints).length > 0) {
|
|
9945
|
+
payload.generation_hints = hints;
|
|
9946
|
+
}
|
|
9947
|
+
body = payload;
|
|
9948
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
8955
9949
|
}
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
9950
|
+
default:
|
|
9951
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
function parseResponse(provider, data) {
|
|
9955
|
+
const r = data;
|
|
9956
|
+
switch (provider) {
|
|
9957
|
+
case "ollama":
|
|
9958
|
+
return r.response ?? "";
|
|
9959
|
+
case "lmstudio":
|
|
9960
|
+
case "openrouter":
|
|
9961
|
+
case "custom": {
|
|
9962
|
+
const choices = r.choices;
|
|
9963
|
+
return choices?.[0]?.message?.content ?? "";
|
|
9964
|
+
}
|
|
9965
|
+
case "cloudflare":
|
|
9966
|
+
return r.commit?.response ?? "";
|
|
9967
|
+
default:
|
|
9968
|
+
return "";
|
|
9969
|
+
}
|
|
9970
|
+
}
|
|
9971
|
+
async function runLocalCommit(args) {
|
|
9972
|
+
const silent = !!(args.hookMode || args.quiet);
|
|
9973
|
+
const ui2 = getUI();
|
|
9974
|
+
const log = silent ? createSilentLog() : ui2.log;
|
|
9975
|
+
if (!isGitRepo()) {
|
|
9976
|
+
throw new Error("Not a git repository.");
|
|
9977
|
+
}
|
|
9978
|
+
if (!hasStagedChanges()) {
|
|
9979
|
+
throw new Error("No staged changes. Stage files with `git add` first.");
|
|
9980
|
+
}
|
|
9981
|
+
const local = getLocalProviderConfig();
|
|
9982
|
+
if (!local) {
|
|
9983
|
+
throw new Error(
|
|
9984
|
+
"No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
|
|
9985
|
+
);
|
|
9986
|
+
}
|
|
9987
|
+
const config2 = getConfig();
|
|
9988
|
+
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
9989
|
+
let diff = getStagedDiff(excludes);
|
|
9990
|
+
const changes = getStagedFiles();
|
|
9991
|
+
if (!args.noSmartDiff) {
|
|
9992
|
+
const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
|
|
9993
|
+
diff = smartResult.processedDiff;
|
|
9994
|
+
if (smartResult.summarized.length > 0 && !silent) {
|
|
9995
|
+
log.step(
|
|
9996
|
+
`smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
|
|
9997
|
+
);
|
|
9998
|
+
}
|
|
9999
|
+
if (smartResult.aggressivelySummarized.length > 0 && !silent) {
|
|
10000
|
+
log.step(
|
|
10001
|
+
`large-diff: ${smartResult.aggressivelySummarized.length} additional file(s) summarized to fit (commit message may be less specific)`
|
|
10002
|
+
);
|
|
10003
|
+
}
|
|
10004
|
+
}
|
|
10005
|
+
let rules = { ...await detectCommitlintRules(), ...config2.rules ?? {} };
|
|
10006
|
+
const workspace = detectWorkspace();
|
|
10007
|
+
if (workspace) {
|
|
10008
|
+
const stagedFiles = changes.trim().split("\n").filter(Boolean);
|
|
10009
|
+
const scope = autoDetectScope(stagedFiles, workspace);
|
|
10010
|
+
if (scope) {
|
|
10011
|
+
const scopes = scope.split(",").map((s) => s.trim());
|
|
10012
|
+
rules = { ...rules, scopes };
|
|
10013
|
+
}
|
|
10014
|
+
}
|
|
10015
|
+
rules = applyCliTypeScopeToRules(rules, args.type, args.scope);
|
|
10016
|
+
const recentCommits = args.noContext ? void 0 : getRecentBranchCommits(5);
|
|
10017
|
+
const generationHints = generationHintsFromArgs(args.split, args.forceBody);
|
|
10018
|
+
const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
10019
|
+
const skipConfirm = args.dryRun || args.messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
10020
|
+
const model = args.model ?? local.model;
|
|
10021
|
+
const modelDisplay = model ?? local.model ?? "default";
|
|
10022
|
+
const userContent = buildUserPrompt(
|
|
10023
|
+
changes,
|
|
10024
|
+
diff,
|
|
10025
|
+
Object.keys(rules).length > 0 ? rules : void 0,
|
|
10026
|
+
recentCommits,
|
|
10027
|
+
generationHints
|
|
10028
|
+
);
|
|
10029
|
+
const { url, body, headers } = buildRequest(
|
|
10030
|
+
local.provider,
|
|
10031
|
+
local.baseUrl,
|
|
10032
|
+
userContent,
|
|
10033
|
+
diff,
|
|
10034
|
+
changes,
|
|
10035
|
+
model,
|
|
10036
|
+
local.apiKey,
|
|
10037
|
+
rules,
|
|
10038
|
+
recentCommits,
|
|
10039
|
+
generationHints
|
|
10040
|
+
);
|
|
10041
|
+
if (!url || url.includes("YOUR-WORKER")) {
|
|
10042
|
+
throw new Error(
|
|
10043
|
+
"Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
|
|
10044
|
+
);
|
|
10045
|
+
}
|
|
10046
|
+
const spinner = ui2.spinner(`generating commit (${modelDisplay} via ${local.provider})...`);
|
|
10047
|
+
if (!silent) spinner.start();
|
|
10048
|
+
const t0 = Date.now();
|
|
10049
|
+
let res;
|
|
10050
|
+
try {
|
|
10051
|
+
res = await fetch(url, {
|
|
10052
|
+
method: "POST",
|
|
10053
|
+
headers,
|
|
10054
|
+
body: JSON.stringify(body)
|
|
10055
|
+
});
|
|
10056
|
+
} finally {
|
|
10057
|
+
spinner.stop();
|
|
10058
|
+
}
|
|
10059
|
+
const roundTripMs = Date.now() - t0;
|
|
10060
|
+
if (!res.ok) {
|
|
10061
|
+
const text = await res.text();
|
|
10062
|
+
throw new Error(`Provider error (${res.status}): ${text}`);
|
|
10063
|
+
}
|
|
10064
|
+
const data = await res.json();
|
|
10065
|
+
let message = parseResponse(local.provider, data);
|
|
10066
|
+
message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
|
|
10067
|
+
if (!message) {
|
|
10068
|
+
throw new Error("Failed to generate commit message.");
|
|
10069
|
+
}
|
|
10070
|
+
const diagnostics = local.provider === "cloudflare" && typeof data === "object" && data !== null ? data.diagnostics : void 0;
|
|
10071
|
+
logVerboseDiagnostics((msg) => log.dim(msg), args.verbose, args.quiet, diagnostics, roundTripMs);
|
|
10072
|
+
if (args.interactive) {
|
|
10073
|
+
if (shouldSkipTTYInteraction(args.hookMode)) {
|
|
10074
|
+
if (!silent) log.dim("(--interactive ignored: not running in a TTY)");
|
|
10075
|
+
} else {
|
|
10076
|
+
const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
|
|
10077
|
+
if (refineResult.action === "abort") {
|
|
10078
|
+
return;
|
|
8961
10079
|
}
|
|
8962
|
-
|
|
8963
|
-
console.log(`Invitation sent to ${email}`);
|
|
8964
|
-
break;
|
|
10080
|
+
message = refineResult.message;
|
|
8965
10081
|
}
|
|
8966
|
-
default:
|
|
8967
|
-
console.error(`Unknown team command: ${subcommand}`);
|
|
8968
|
-
console.log("Usage: qc team [info|rules|rules push|invite <email>]");
|
|
8969
|
-
process.exit(1);
|
|
8970
10082
|
}
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
"src/commands/team.ts"() {
|
|
8975
|
-
"use strict";
|
|
8976
|
-
import_fs9 = require("fs");
|
|
8977
|
-
import_path9 = require("path");
|
|
8978
|
-
init_api();
|
|
8979
|
-
init_config();
|
|
10083
|
+
if (args.messageOnly) {
|
|
10084
|
+
console.log(message);
|
|
10085
|
+
return;
|
|
8980
10086
|
}
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
10087
|
+
if (!silent) {
|
|
10088
|
+
const stagedPaths = changes.trim().split("\n").filter(Boolean);
|
|
10089
|
+
const short = getStagedDiffShortstat();
|
|
10090
|
+
const tokenEst = diagnostics && typeof diagnostics === "object" && diagnostics !== null && "tokenUsage" in diagnostics ? diagnostics.tokenUsage?.totalEstimated : void 0;
|
|
10091
|
+
displayCommitMessage(message, {
|
|
10092
|
+
log,
|
|
10093
|
+
isColor: ui2.isColor,
|
|
10094
|
+
isTTY: !!process.stderr.isTTY,
|
|
10095
|
+
style: "rich",
|
|
10096
|
+
stagedFiles: stagedPaths,
|
|
10097
|
+
stats: {
|
|
10098
|
+
files: stagedPaths.length,
|
|
10099
|
+
additions: short.additions,
|
|
10100
|
+
deletions: short.deletions,
|
|
10101
|
+
...tokenEst !== void 0 ? { tokens: tokenEst } : {}
|
|
10102
|
+
}
|
|
10103
|
+
});
|
|
10104
|
+
}
|
|
10105
|
+
if (args.dryRun) {
|
|
8991
10106
|
return;
|
|
8992
10107
|
}
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
if (!key || !value) {
|
|
8998
|
-
console.error("Usage: qc config set <key> <value>");
|
|
8999
|
-
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
9000
|
-
process.exit(1);
|
|
10108
|
+
if (args.confirm) {
|
|
10109
|
+
const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
|
|
10110
|
+
if (confirmResult.action === "abort") {
|
|
10111
|
+
return;
|
|
9001
10112
|
}
|
|
9002
|
-
setConfig(key, value);
|
|
9003
|
-
return;
|
|
9004
10113
|
}
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
10114
|
+
gitCommit(message);
|
|
10115
|
+
if (args.push) {
|
|
10116
|
+
gitPush();
|
|
9008
10117
|
}
|
|
9009
|
-
console.error(`Unknown subcommand: ${sub}`);
|
|
9010
|
-
console.error("Usage: qc config [set <key> <value> | reset]");
|
|
9011
|
-
process.exit(1);
|
|
9012
10118
|
}
|
|
9013
|
-
function
|
|
9014
|
-
const
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
10119
|
+
async function generateLocalBranchName(opts) {
|
|
10120
|
+
const local = getLocalProviderConfig();
|
|
10121
|
+
if (!local) {
|
|
10122
|
+
throw new Error("No local provider configured. Set provider with `qc --use-ollama` etc.");
|
|
10123
|
+
}
|
|
10124
|
+
const sections = [];
|
|
10125
|
+
sections.push("Generate a git branch name in the format <type>/<kebab-case-slug>.");
|
|
10126
|
+
sections.push("Type must be one of: feat, fix, refactor, perf, docs, test, chore, ci.");
|
|
10127
|
+
sections.push("Slug: 2-5 words, lowercase, hyphen-separated, max 55 chars.");
|
|
10128
|
+
sections.push("Output ONLY the branch name on a single line. No explanation.");
|
|
10129
|
+
sections.push("");
|
|
10130
|
+
if (opts.description) {
|
|
10131
|
+
sections.push("DESCRIPTION:");
|
|
10132
|
+
sections.push(opts.description);
|
|
10133
|
+
} else if (opts.recentCommits && opts.recentCommits.length > 0) {
|
|
10134
|
+
sections.push("RECENT COMMITS:");
|
|
10135
|
+
for (const c of opts.recentCommits) sections.push(`- ${c}`);
|
|
10136
|
+
} else if (opts.diff) {
|
|
10137
|
+
sections.push("DIFF:");
|
|
10138
|
+
sections.push(opts.diff.slice(0, 3e4));
|
|
10139
|
+
}
|
|
10140
|
+
const userContent = sections.join("\n");
|
|
10141
|
+
const model = opts.model ?? local.model;
|
|
10142
|
+
const headers = { "Content-Type": "application/json" };
|
|
10143
|
+
if (local.apiKey) headers.Authorization = `Bearer ${local.apiKey}`;
|
|
10144
|
+
if (local.provider === "openrouter") {
|
|
10145
|
+
headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
|
|
10146
|
+
headers["X-Title"] = "qc - AI Commit Message Generator";
|
|
9024
10147
|
}
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
10148
|
+
let url;
|
|
10149
|
+
let body;
|
|
10150
|
+
switch (local.provider) {
|
|
10151
|
+
case "ollama":
|
|
10152
|
+
url = `${local.baseUrl}/api/generate`;
|
|
10153
|
+
body = { model, prompt: userContent, stream: false, options: {} };
|
|
10154
|
+
break;
|
|
10155
|
+
case "lmstudio":
|
|
10156
|
+
case "openrouter":
|
|
10157
|
+
case "custom":
|
|
10158
|
+
url = `${local.baseUrl}/chat/completions`;
|
|
10159
|
+
body = {
|
|
10160
|
+
model,
|
|
10161
|
+
stream: false,
|
|
10162
|
+
messages: [
|
|
10163
|
+
{
|
|
10164
|
+
role: "system",
|
|
10165
|
+
content: "You suggest concise git branch names. Reply with the branch name only."
|
|
10166
|
+
},
|
|
10167
|
+
{ role: "user", content: userContent }
|
|
10168
|
+
]
|
|
10169
|
+
};
|
|
10170
|
+
break;
|
|
10171
|
+
case "cloudflare":
|
|
10172
|
+
url = `${local.baseUrl.replace(/\/$/, "")}/branch`;
|
|
10173
|
+
body = {
|
|
10174
|
+
diff: opts.diff,
|
|
10175
|
+
changes: opts.changes,
|
|
10176
|
+
recent_commits: opts.recentCommits,
|
|
10177
|
+
description: opts.description,
|
|
10178
|
+
model,
|
|
10179
|
+
cf_model: model,
|
|
10180
|
+
...opts.rules ? { rules: opts.rules } : {}
|
|
10181
|
+
};
|
|
10182
|
+
break;
|
|
9052
10183
|
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
saveConfig({});
|
|
9058
|
-
console.log("Config reset to defaults.");
|
|
9059
|
-
}
|
|
9060
|
-
var init_config2 = __esm({
|
|
9061
|
-
"src/commands/config.ts"() {
|
|
9062
|
-
"use strict";
|
|
9063
|
-
init_config();
|
|
9064
|
-
init_dist();
|
|
10184
|
+
if (!url || url.includes("YOUR-WORKER")) {
|
|
10185
|
+
throw new Error(
|
|
10186
|
+
"Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
|
|
10187
|
+
);
|
|
9065
10188
|
}
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
// src/commands/upgrade.ts
|
|
9069
|
-
var upgrade_exports = {};
|
|
9070
|
-
__export(upgrade_exports, {
|
|
9071
|
-
upgrade: () => upgrade
|
|
9072
|
-
});
|
|
9073
|
-
async function upgrade() {
|
|
9074
|
-
console.log(`
|
|
9075
|
-
Opening ${BILLING_URL}
|
|
9076
|
-
`);
|
|
10189
|
+
let res;
|
|
9077
10190
|
try {
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
} else if (process.platform === "win32") {
|
|
9084
|
-
execFileSync7("cmd", ["/c", "start", "", BILLING_URL]);
|
|
9085
|
-
}
|
|
10191
|
+
res = await fetch(url, {
|
|
10192
|
+
method: "POST",
|
|
10193
|
+
headers,
|
|
10194
|
+
body: JSON.stringify(body)
|
|
10195
|
+
});
|
|
9086
10196
|
} catch {
|
|
9087
|
-
|
|
10197
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10198
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
9088
10199
|
}
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
"src/commands/upgrade.ts"() {
|
|
9093
|
-
"use strict";
|
|
9094
|
-
BILLING_URL = "https://app.quikcommit.dev/billing";
|
|
10200
|
+
if (!res.ok) {
|
|
10201
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10202
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
9095
10203
|
}
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
for (let i = 1; i < parts.length; i += 2) {
|
|
9114
|
-
const header = parts[i];
|
|
9115
|
-
const content = parts[i + 1] ?? "";
|
|
9116
|
-
const match = header.match(/diff --git a\/(.+?) b\/(.+)/);
|
|
9117
|
-
const filepath = match?.[2] ?? "unknown";
|
|
9118
|
-
const lines = content.split("\n");
|
|
9119
|
-
let additions = 0;
|
|
9120
|
-
let deletions = 0;
|
|
9121
|
-
for (const line of lines) {
|
|
9122
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
9123
|
-
else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
|
|
9124
|
-
}
|
|
9125
|
-
files.push({ filepath, content: header + content, additions, deletions });
|
|
10204
|
+
const data = await res.json();
|
|
10205
|
+
let raw;
|
|
10206
|
+
if (local.provider === "cloudflare") {
|
|
10207
|
+
const r = data;
|
|
10208
|
+
const br = r.branch;
|
|
10209
|
+
raw = typeof br?.name === "string" ? br.name : "";
|
|
10210
|
+
} else if (local.provider === "ollama") {
|
|
10211
|
+
raw = data.response ?? "";
|
|
10212
|
+
} else {
|
|
10213
|
+
const choices = data.choices;
|
|
10214
|
+
raw = choices?.[0]?.message?.content ?? "";
|
|
10215
|
+
}
|
|
10216
|
+
raw = raw.replace(/[\r\n].*$/s, "").trim();
|
|
10217
|
+
const sanitized = sanitizeBranchName(raw);
|
|
10218
|
+
if (!sanitized) {
|
|
10219
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10220
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
9126
10221
|
}
|
|
9127
|
-
return
|
|
9128
|
-
}
|
|
9129
|
-
function isMinified(content) {
|
|
9130
|
-
const lines = content.split("\n").filter(
|
|
9131
|
-
(l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---")
|
|
9132
|
-
);
|
|
9133
|
-
if (lines.length === 0) return false;
|
|
9134
|
-
return lines.some((l) => l.length > 500);
|
|
10222
|
+
return ensureUniqueName(sanitized, branchExists);
|
|
9135
10223
|
}
|
|
9136
|
-
function
|
|
9137
|
-
const
|
|
9138
|
-
if (
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
kept.push(file.content);
|
|
9176
|
-
}
|
|
9177
|
-
break;
|
|
9178
|
-
}
|
|
10224
|
+
async function runLocalBranch(opts) {
|
|
10225
|
+
const local = getLocalProviderConfig();
|
|
10226
|
+
if (!local) {
|
|
10227
|
+
throw new Error("No local provider configured. Set provider with `qc --use-ollama` etc.");
|
|
10228
|
+
}
|
|
10229
|
+
const ui2 = getUI();
|
|
10230
|
+
const log = ui2.log;
|
|
10231
|
+
const spinner = ui2.spinner(`generating branch name (${opts.model ?? local.model} via ${local.provider})...`);
|
|
10232
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10233
|
+
let final;
|
|
10234
|
+
try {
|
|
10235
|
+
final = await generateLocalBranchName({
|
|
10236
|
+
description: opts.description,
|
|
10237
|
+
diff: opts.diff,
|
|
10238
|
+
changes: opts.changes,
|
|
10239
|
+
recentCommits: opts.recentCommits,
|
|
10240
|
+
model: opts.model,
|
|
10241
|
+
rules: opts.rules
|
|
10242
|
+
});
|
|
10243
|
+
} catch {
|
|
10244
|
+
const filesArr = opts.changes?.split("\n").filter(Boolean) ?? [];
|
|
10245
|
+
const fallback = deterministicBranchName({ files: filesArr, description: opts.description });
|
|
10246
|
+
final = ensureUniqueName(fallback.name, branchExists);
|
|
10247
|
+
log.dim("(used local fallback name; AI generation failed)");
|
|
10248
|
+
} finally {
|
|
10249
|
+
spinner.stop();
|
|
10250
|
+
}
|
|
10251
|
+
log.success(`branch name: ${final}`);
|
|
10252
|
+
const baseRef = opts.baseRef ?? "HEAD";
|
|
10253
|
+
if (opts.noSwitch) {
|
|
10254
|
+
createBranch(final, baseRef);
|
|
10255
|
+
log.success(`created ${final} (not switched)`);
|
|
10256
|
+
} else {
|
|
10257
|
+
createAndCheckoutBranch(final, baseRef);
|
|
10258
|
+
log.success(`switched to ${final}`);
|
|
10259
|
+
}
|
|
10260
|
+
if (opts.push) {
|
|
10261
|
+
gitPushSetUpstream(final);
|
|
10262
|
+
log.success(`pushed origin/${final}`);
|
|
9179
10263
|
}
|
|
9180
|
-
return {
|
|
9181
|
-
processedDiff: kept.join(""),
|
|
9182
|
-
summarized,
|
|
9183
|
-
tokensSaved
|
|
9184
|
-
};
|
|
9185
10264
|
}
|
|
9186
|
-
var
|
|
9187
|
-
var
|
|
9188
|
-
"src/
|
|
10265
|
+
var import_fs8, import_path8, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
|
|
10266
|
+
var init_local = __esm({
|
|
10267
|
+
"src/local.ts"() {
|
|
9189
10268
|
"use strict";
|
|
10269
|
+
import_fs8 = require("fs");
|
|
10270
|
+
import_path8 = require("path");
|
|
10271
|
+
import_os4 = require("os");
|
|
10272
|
+
init_config();
|
|
9190
10273
|
init_dist();
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
"
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
/\.pb\.go$/,
|
|
9206
|
-
/\.pb\.ts$/,
|
|
9207
|
-
/(^|\/)\.prisma\/client\//,
|
|
9208
|
-
/\/generated\//
|
|
9209
|
-
];
|
|
9210
|
-
VENDORED_PREFIXES = ["vendor/", "third_party/", "node_modules/"];
|
|
9211
|
-
}
|
|
9212
|
-
});
|
|
9213
|
-
|
|
9214
|
-
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
9215
|
-
var require_picocolors = __commonJS({
|
|
9216
|
-
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports2, module2) {
|
|
9217
|
-
var p = process || {};
|
|
9218
|
-
var argv = p.argv || [];
|
|
9219
|
-
var env = p.env || {};
|
|
9220
|
-
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
9221
|
-
var formatter = (open, close, replace = open) => (input) => {
|
|
9222
|
-
let string = "" + input, index = string.indexOf(close, open.length);
|
|
9223
|
-
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
9224
|
-
};
|
|
9225
|
-
var replaceClose = (string, close, replace, index) => {
|
|
9226
|
-
let result = "", cursor = 0;
|
|
9227
|
-
do {
|
|
9228
|
-
result += string.substring(cursor, index) + replace;
|
|
9229
|
-
cursor = index + close.length;
|
|
9230
|
-
index = string.indexOf(close, cursor);
|
|
9231
|
-
} while (~index);
|
|
9232
|
-
return result + string.substring(cursor);
|
|
10274
|
+
init_git();
|
|
10275
|
+
init_monorepo();
|
|
10276
|
+
init_commitlint();
|
|
10277
|
+
init_smart_diff();
|
|
10278
|
+
init_ui();
|
|
10279
|
+
init_commit_helpers();
|
|
10280
|
+
init_branch_name();
|
|
10281
|
+
CONFIG_PATH2 = (0, import_path8.join)((0, import_os4.homedir)(), CONFIG_DIR);
|
|
10282
|
+
PROVIDER_URLS = {
|
|
10283
|
+
ollama: "http://localhost:11434",
|
|
10284
|
+
lmstudio: "http://localhost:1234/v1",
|
|
10285
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
10286
|
+
custom: "",
|
|
10287
|
+
cloudflare: ""
|
|
9233
10288
|
};
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
9241
|
-
italic: f("\x1B[3m", "\x1B[23m"),
|
|
9242
|
-
underline: f("\x1B[4m", "\x1B[24m"),
|
|
9243
|
-
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
9244
|
-
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
9245
|
-
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
9246
|
-
black: f("\x1B[30m", "\x1B[39m"),
|
|
9247
|
-
red: f("\x1B[31m", "\x1B[39m"),
|
|
9248
|
-
green: f("\x1B[32m", "\x1B[39m"),
|
|
9249
|
-
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
9250
|
-
blue: f("\x1B[34m", "\x1B[39m"),
|
|
9251
|
-
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
9252
|
-
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
9253
|
-
white: f("\x1B[37m", "\x1B[39m"),
|
|
9254
|
-
gray: f("\x1B[90m", "\x1B[39m"),
|
|
9255
|
-
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
9256
|
-
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
9257
|
-
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
9258
|
-
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
9259
|
-
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
9260
|
-
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
9261
|
-
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
9262
|
-
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
9263
|
-
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
9264
|
-
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
9265
|
-
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
9266
|
-
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
9267
|
-
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
9268
|
-
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
9269
|
-
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
9270
|
-
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
9271
|
-
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
9272
|
-
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
9273
|
-
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
9274
|
-
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
9275
|
-
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
9276
|
-
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
9277
|
-
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
9278
|
-
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
9279
|
-
};
|
|
10289
|
+
DEFAULT_MODELS = {
|
|
10290
|
+
ollama: "codellama",
|
|
10291
|
+
lmstudio: "default",
|
|
10292
|
+
openrouter: "google/gemini-flash-1.5-8b",
|
|
10293
|
+
custom: "",
|
|
10294
|
+
cloudflare: "@cf/qwen/qwen2.5-coder-32b-instruct"
|
|
9280
10295
|
};
|
|
9281
|
-
module2.exports = createColors();
|
|
9282
|
-
module2.exports.createColors = createColors;
|
|
9283
10296
|
}
|
|
9284
10297
|
});
|
|
9285
10298
|
|
|
9286
|
-
// src/
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
10299
|
+
// src/commands/branch.ts
|
|
10300
|
+
var branch_exports = {};
|
|
10301
|
+
__export(branch_exports, {
|
|
10302
|
+
runBranch: () => runBranch
|
|
10303
|
+
});
|
|
10304
|
+
function branchGenerationRules(cfg) {
|
|
10305
|
+
const types = cfg.branch?.generation?.types;
|
|
10306
|
+
if (types && types.length > 0) return { types: [...types] };
|
|
10307
|
+
return void 0;
|
|
9293
10308
|
}
|
|
9294
|
-
function
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
10309
|
+
function finalizeGeneratedBranchName(raw) {
|
|
10310
|
+
return finalizeBranchName(raw, branchExists);
|
|
10311
|
+
}
|
|
10312
|
+
async function runBranch(opts) {
|
|
10313
|
+
const ui2 = getUI();
|
|
10314
|
+
const log = ui2.log;
|
|
10315
|
+
if (!isGitRepo()) {
|
|
10316
|
+
log.error("Not a git repository.");
|
|
10317
|
+
process.exit(1);
|
|
10318
|
+
}
|
|
10319
|
+
const baseRef = opts.from ?? "HEAD";
|
|
10320
|
+
const config2 = getConfig();
|
|
10321
|
+
const model = opts.model ?? config2.model;
|
|
10322
|
+
const genRules = branchGenerationRules(config2);
|
|
10323
|
+
if (opts.rescue) {
|
|
10324
|
+
const state = detectProtectedBranchState({
|
|
10325
|
+
protectedBranches: config2.branch?.protectedBranches,
|
|
10326
|
+
detectDefault: config2.branch?.detectDefault
|
|
10327
|
+
});
|
|
10328
|
+
if (!state.isProtected) {
|
|
10329
|
+
throw new Error(
|
|
10330
|
+
"`--rescue` only applies on a protected branch (e.g. main). The current branch is not protected."
|
|
10331
|
+
);
|
|
10332
|
+
}
|
|
10333
|
+
if (state.commitsAhead === 0) {
|
|
10334
|
+
throw new Error(
|
|
10335
|
+
"No commits ahead of upstream to rescue. Push your branch or use `qc branch` without `--rescue`."
|
|
10336
|
+
);
|
|
10337
|
+
}
|
|
10338
|
+
let final2;
|
|
10339
|
+
if (opts.explicitName) {
|
|
10340
|
+
const sanitized = sanitizeBranchName(opts.explicitName);
|
|
10341
|
+
if (!sanitized) {
|
|
10342
|
+
throw new Error(`invalid branch name: ${opts.explicitName}`);
|
|
10343
|
+
}
|
|
10344
|
+
final2 = finalizeBranchName(sanitized, branchExists);
|
|
10345
|
+
} else {
|
|
10346
|
+
const recent = getRecentBranchCommits(state.commitsAhead);
|
|
10347
|
+
const apiKey2 = opts.apiKey ?? getApiKey();
|
|
10348
|
+
if (apiKey2) {
|
|
10349
|
+
const spinner2 = ui2.spinner(`generating branch name (${model ?? "default"})...`);
|
|
10350
|
+
if (process.stderr.isTTY) spinner2.start();
|
|
10351
|
+
try {
|
|
10352
|
+
const client = new ApiClient({ apiKey: apiKey2 });
|
|
10353
|
+
try {
|
|
10354
|
+
const result2 = await client.generateBranchName({
|
|
10355
|
+
recent_commits: recent,
|
|
10356
|
+
model: opts.model,
|
|
10357
|
+
description: opts.message,
|
|
10358
|
+
rules: genRules
|
|
10359
|
+
});
|
|
10360
|
+
final2 = finalizeGeneratedBranchName(result2.name);
|
|
10361
|
+
} catch {
|
|
10362
|
+
const fallback = deterministicBranchName({
|
|
10363
|
+
description: recent.join(" ") || opts.message
|
|
10364
|
+
});
|
|
10365
|
+
final2 = finalizeBranchName(fallback.name, branchExists);
|
|
10366
|
+
log.dim("(used deterministic fallback name; API generation failed)");
|
|
10367
|
+
}
|
|
10368
|
+
} finally {
|
|
10369
|
+
spinner2.stop();
|
|
9325
10370
|
}
|
|
9326
|
-
|
|
9327
|
-
|
|
10371
|
+
} else {
|
|
10372
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, generateLocalBranchName: generateLocalBranchName2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10373
|
+
if (!getLocalProviderConfig2()) {
|
|
10374
|
+
throw new Error(
|
|
10375
|
+
"Not authenticated. Run `qc login` first, or configure a local provider for `--rescue`."
|
|
10376
|
+
);
|
|
10377
|
+
}
|
|
10378
|
+
const spinner2 = ui2.spinner(`generating branch name (${model ?? "default"} via local)...`);
|
|
10379
|
+
if (process.stderr.isTTY) spinner2.start();
|
|
10380
|
+
try {
|
|
10381
|
+
try {
|
|
10382
|
+
const name = await generateLocalBranchName2({
|
|
10383
|
+
recentCommits: recent,
|
|
10384
|
+
model: opts.model,
|
|
10385
|
+
description: opts.message,
|
|
10386
|
+
rules: genRules
|
|
10387
|
+
});
|
|
10388
|
+
final2 = finalizeBranchName(name, branchExists, { skipUniqueness: true });
|
|
10389
|
+
} catch {
|
|
10390
|
+
const fallback = deterministicBranchName({
|
|
10391
|
+
description: recent.join(" ") || opts.message
|
|
10392
|
+
});
|
|
10393
|
+
final2 = finalizeBranchName(fallback.name, branchExists);
|
|
10394
|
+
log.dim("(used deterministic fallback name; local provider failed)");
|
|
10395
|
+
}
|
|
10396
|
+
} finally {
|
|
10397
|
+
spinner2.stop();
|
|
9328
10398
|
}
|
|
9329
10399
|
}
|
|
9330
|
-
}
|
|
10400
|
+
}
|
|
10401
|
+
log.success(`branch name: ${final2}`);
|
|
10402
|
+
if (opts.dryRun) {
|
|
10403
|
+
log.dim("(dry-run; not running rescue)");
|
|
10404
|
+
return;
|
|
10405
|
+
}
|
|
10406
|
+
if (!process.stdin.isTTY) {
|
|
10407
|
+
throw new Error("`--rescue` requires an interactive terminal to confirm (or use `qc branch <name>` after arranging commits manually).");
|
|
10408
|
+
}
|
|
10409
|
+
log.dim(
|
|
10410
|
+
`About to: 1) create ${final2} at HEAD, 2) reset ${state.branch} to upstream, 3) switch to ${final2}`
|
|
10411
|
+
);
|
|
10412
|
+
if (!await promptYesNo("Continue with rescue?")) {
|
|
10413
|
+
log.dim("aborted.");
|
|
10414
|
+
return;
|
|
10415
|
+
}
|
|
10416
|
+
rescueCommits({ currentBranch: state.branch, newBranch: final2 });
|
|
10417
|
+
log.success(`moved ${state.commitsAhead} commit(s) to ${final2}`);
|
|
10418
|
+
log.success(`${state.branch} reset to upstream`);
|
|
10419
|
+
if (opts.push) {
|
|
10420
|
+
gitPushSetUpstream(final2);
|
|
10421
|
+
log.success(`pushed origin/${final2}`);
|
|
10422
|
+
}
|
|
10423
|
+
return;
|
|
10424
|
+
}
|
|
10425
|
+
if (opts.explicitName) {
|
|
10426
|
+
const sanitized = sanitizeBranchName(opts.explicitName);
|
|
10427
|
+
if (!sanitized) {
|
|
10428
|
+
throw new Error(`invalid branch name: ${opts.explicitName}`);
|
|
10429
|
+
}
|
|
10430
|
+
const final2 = finalizeBranchName(sanitized, branchExists);
|
|
10431
|
+
if (opts.dryRun) {
|
|
10432
|
+
log.success(`would create branch: ${final2}`);
|
|
10433
|
+
return;
|
|
10434
|
+
}
|
|
10435
|
+
if (opts.noSwitch) {
|
|
10436
|
+
createBranch(final2, baseRef);
|
|
10437
|
+
log.success(`created branch ${final2} (not switched)`);
|
|
10438
|
+
} else {
|
|
10439
|
+
createAndCheckoutBranch(final2, baseRef);
|
|
10440
|
+
log.success(`switched to ${final2}`);
|
|
10441
|
+
}
|
|
10442
|
+
if (opts.push) {
|
|
10443
|
+
gitPushSetUpstream(final2);
|
|
10444
|
+
log.success(`pushed origin/${final2}`);
|
|
10445
|
+
}
|
|
10446
|
+
return;
|
|
9331
10447
|
}
|
|
9332
|
-
const
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
}
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
});
|
|
10448
|
+
const payload = { model, rules: genRules };
|
|
10449
|
+
if (opts.message) {
|
|
10450
|
+
payload.description = opts.message;
|
|
10451
|
+
} else if (opts.fromCommits) {
|
|
10452
|
+
payload.recent_commits = getRecentBranchCommits(10);
|
|
10453
|
+
} else {
|
|
10454
|
+
if (!hasStagedChanges()) {
|
|
10455
|
+
throw new Error(
|
|
10456
|
+
"No staged changes detected. Stage with `git add`, or provide -m '<description>'."
|
|
10457
|
+
);
|
|
10458
|
+
}
|
|
10459
|
+
payload.diff = getStagedDiff(config2.excludes ?? []);
|
|
10460
|
+
payload.changes = getStagedFiles();
|
|
9346
10461
|
}
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
10462
|
+
const apiKey = opts.apiKey ?? getApiKey();
|
|
10463
|
+
if (!apiKey) {
|
|
10464
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, runLocalBranch: runLocalBranch2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10465
|
+
if (getLocalProviderConfig2()) {
|
|
10466
|
+
await runLocalBranch2({
|
|
10467
|
+
description: opts.message,
|
|
10468
|
+
diff: opts.message ? void 0 : payload.diff,
|
|
10469
|
+
changes: opts.message ? void 0 : payload.changes,
|
|
10470
|
+
recentCommits: payload.recent_commits,
|
|
10471
|
+
model: opts.model,
|
|
10472
|
+
noSwitch: opts.noSwitch,
|
|
10473
|
+
push: opts.push,
|
|
10474
|
+
baseRef,
|
|
10475
|
+
rules: genRules
|
|
10476
|
+
});
|
|
10477
|
+
return;
|
|
10478
|
+
}
|
|
10479
|
+
throw new Error("Not authenticated. Run `qc login` first, or provide --message.");
|
|
9360
10480
|
}
|
|
9361
|
-
});
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
10481
|
+
const spinner = ui2.spinner(`generating branch name (${model ?? "default"})...`);
|
|
10482
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10483
|
+
let result;
|
|
10484
|
+
try {
|
|
10485
|
+
const client = new ApiClient({ apiKey });
|
|
10486
|
+
result = await client.generateBranchName(payload);
|
|
10487
|
+
} finally {
|
|
10488
|
+
spinner.stop();
|
|
9368
10489
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
10490
|
+
const final = finalizeGeneratedBranchName(result.name);
|
|
10491
|
+
log.success(`branch name: ${final}`);
|
|
10492
|
+
if (opts.dryRun) {
|
|
10493
|
+
log.dim(`(dry-run; not creating)`);
|
|
10494
|
+
return;
|
|
9371
10495
|
}
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
return Object.keys(h).length > 0 ? h : void 0;
|
|
9379
|
-
}
|
|
9380
|
-
function splitCommitMessageForDisplay(message) {
|
|
9381
|
-
const t = message.replace(/\r\n/g, "\n").trimEnd();
|
|
9382
|
-
const doubleNl = t.indexOf("\n\n");
|
|
9383
|
-
if (doubleNl !== -1) {
|
|
9384
|
-
const head = t.slice(0, doubleNl);
|
|
9385
|
-
const subject = head.split("\n")[0]?.trim() ?? "";
|
|
9386
|
-
return { subject, body: t.slice(doubleNl + 2).trimEnd() };
|
|
10496
|
+
if (opts.noSwitch) {
|
|
10497
|
+
createBranch(final, baseRef);
|
|
10498
|
+
log.success(`created ${final} (not switched)`);
|
|
10499
|
+
} else {
|
|
10500
|
+
createAndCheckoutBranch(final, baseRef);
|
|
10501
|
+
log.success(`switched to ${final}`);
|
|
9387
10502
|
}
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
10503
|
+
if (opts.push) {
|
|
10504
|
+
gitPushSetUpstream(final);
|
|
10505
|
+
log.success(`pushed origin/${final}`);
|
|
9391
10506
|
}
|
|
9392
|
-
return {
|
|
9393
|
-
subject: t.slice(0, firstNl).trim(),
|
|
9394
|
-
body: t.slice(firstNl + 1).trimEnd()
|
|
9395
|
-
};
|
|
9396
10507
|
}
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
10508
|
+
var init_branch2 = __esm({
|
|
10509
|
+
"src/commands/branch.ts"() {
|
|
10510
|
+
"use strict";
|
|
10511
|
+
init_api();
|
|
10512
|
+
init_config();
|
|
10513
|
+
init_branch_rescue();
|
|
10514
|
+
init_protected_branch_guard();
|
|
10515
|
+
init_git();
|
|
10516
|
+
init_branch_name();
|
|
10517
|
+
init_commit_helpers();
|
|
10518
|
+
init_ui();
|
|
9401
10519
|
}
|
|
9402
|
-
|
|
9403
|
-
}
|
|
9404
|
-
async function interactiveRefineMessage(initial, opts) {
|
|
9405
|
-
if (opts.skip) return { action: "accept", message: initial };
|
|
9406
|
-
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9407
|
-
try {
|
|
9408
|
-
process.stderr.write(`
|
|
9409
|
-
${initial}
|
|
10520
|
+
});
|
|
9410
10521
|
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
10522
|
+
// src/commands/init.ts
|
|
10523
|
+
var init_exports = {};
|
|
10524
|
+
__export(init_exports, {
|
|
10525
|
+
init: () => init
|
|
10526
|
+
});
|
|
10527
|
+
function init(options) {
|
|
10528
|
+
let hooksDir;
|
|
10529
|
+
try {
|
|
10530
|
+
hooksDir = (0, import_child_process6.execFileSync)("git", ["rev-parse", "--git-path", "hooks"], {
|
|
10531
|
+
encoding: "utf-8"
|
|
10532
|
+
}).trim();
|
|
10533
|
+
} catch {
|
|
10534
|
+
console.error("Error: Not a git repository");
|
|
10535
|
+
process.exit(1);
|
|
10536
|
+
}
|
|
10537
|
+
const hookPath = (0, import_path9.join)(hooksDir, "prepare-commit-msg");
|
|
10538
|
+
if (options.uninstall) {
|
|
10539
|
+
if ((0, import_fs9.existsSync)(hookPath)) {
|
|
10540
|
+
const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
|
|
10541
|
+
if (content.includes("Quikcommit")) {
|
|
10542
|
+
(0, import_fs9.unlinkSync)(hookPath);
|
|
10543
|
+
console.log("Quikcommit hook removed.");
|
|
10544
|
+
} else {
|
|
10545
|
+
console.log("Hook exists but was not installed by Quikcommit. Skipping.");
|
|
9423
10546
|
}
|
|
9424
|
-
|
|
9425
|
-
|
|
10547
|
+
} else {
|
|
10548
|
+
console.log("No hook to remove.");
|
|
9426
10549
|
}
|
|
9427
|
-
return
|
|
9428
|
-
} finally {
|
|
9429
|
-
rl.close();
|
|
10550
|
+
return;
|
|
9430
10551
|
}
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
const ans = (await rl.question(prompt2)).trim().toLowerCase();
|
|
9437
|
-
if (ans !== "y" && ans !== "yes") {
|
|
9438
|
-
return { action: "abort" };
|
|
10552
|
+
if ((0, import_fs9.existsSync)(hookPath)) {
|
|
10553
|
+
const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
|
|
10554
|
+
if (content.includes("Quikcommit")) {
|
|
10555
|
+
console.log("Quikcommit hook is already installed.");
|
|
10556
|
+
return;
|
|
9439
10557
|
}
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
10558
|
+
console.error(
|
|
10559
|
+
"A prepare-commit-msg hook already exists. Use --uninstall first or manually merge."
|
|
10560
|
+
);
|
|
10561
|
+
process.exit(1);
|
|
9443
10562
|
}
|
|
10563
|
+
(0, import_fs9.writeFileSync)(hookPath, HOOK_CONTENT);
|
|
10564
|
+
(0, import_fs9.chmodSync)(hookPath, 493);
|
|
10565
|
+
console.log("Quikcommit hook installed.");
|
|
10566
|
+
console.log("Now just run `git commit` and a message will be generated automatically.");
|
|
9444
10567
|
}
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
10568
|
+
var import_fs9, import_path9, import_child_process6, HOOK_CONTENT;
|
|
10569
|
+
var init_init = __esm({
|
|
10570
|
+
"src/commands/init.ts"() {
|
|
10571
|
+
"use strict";
|
|
10572
|
+
import_fs9 = require("fs");
|
|
10573
|
+
import_path9 = require("path");
|
|
10574
|
+
import_child_process6 = require("child_process");
|
|
10575
|
+
HOOK_CONTENT = `#!/bin/sh
|
|
10576
|
+
# Quikcommit - auto-generate commit messages
|
|
10577
|
+
# Installed by: qc init
|
|
10578
|
+
# Remove with: qc init --uninstall
|
|
10579
|
+
|
|
10580
|
+
# Only generate if no message was provided (empty commit message file)
|
|
10581
|
+
COMMIT_MSG_FILE="$1"
|
|
10582
|
+
COMMIT_SOURCE="$2"
|
|
10583
|
+
|
|
10584
|
+
# Skip if message was provided via -m, merge, squash, etc.
|
|
10585
|
+
if [ -n "$COMMIT_SOURCE" ]; then
|
|
10586
|
+
exit 0
|
|
10587
|
+
fi
|
|
10588
|
+
|
|
10589
|
+
# Skip if message file already has content (excluding comments)
|
|
10590
|
+
if grep -qv '^#' "$COMMIT_MSG_FILE" 2>/dev/null; then
|
|
10591
|
+
if [ -n "$(grep -v '^#' "$COMMIT_MSG_FILE" | grep -v '^$')" ]; then
|
|
10592
|
+
exit 0
|
|
10593
|
+
fi
|
|
10594
|
+
fi
|
|
10595
|
+
|
|
10596
|
+
# Generate commit message
|
|
10597
|
+
MSG=$(qc --message-only --hook-mode 2>/dev/null)
|
|
10598
|
+
if [ $? -eq 0 ] && [ -n "$MSG" ]; then
|
|
10599
|
+
printf '%s
|
|
10600
|
+
' "$MSG" > "$COMMIT_MSG_FILE"
|
|
10601
|
+
fi
|
|
10602
|
+
`;
|
|
10603
|
+
}
|
|
10604
|
+
});
|
|
10605
|
+
|
|
10606
|
+
// src/commands/team.ts
|
|
10607
|
+
var team_exports = {};
|
|
10608
|
+
__export(team_exports, {
|
|
10609
|
+
team: () => team
|
|
10610
|
+
});
|
|
10611
|
+
function createApiClient() {
|
|
10612
|
+
return new ApiClient();
|
|
9456
10613
|
}
|
|
9457
|
-
function
|
|
9458
|
-
return
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
}
|
|
10614
|
+
function mapCommitlintToRules(config2) {
|
|
10615
|
+
if (!config2 || typeof config2 !== "object") return null;
|
|
10616
|
+
const c = config2;
|
|
10617
|
+
const rules = {};
|
|
10618
|
+
const _ext = c.extends;
|
|
10619
|
+
const rulesConfig = c.rules;
|
|
10620
|
+
if (Array.isArray(rulesConfig?.["type-enum"]) && rulesConfig["type-enum"].length >= 3) {
|
|
10621
|
+
const [, , value] = rulesConfig["type-enum"];
|
|
10622
|
+
if (Array.isArray(value)) rules.types = value;
|
|
10623
|
+
}
|
|
10624
|
+
if (Array.isArray(rulesConfig?.["scope-enum"]) && rulesConfig["scope-enum"].length >= 3) {
|
|
10625
|
+
const [, , value] = rulesConfig["scope-enum"];
|
|
10626
|
+
if (Array.isArray(value)) rules.scopes = value;
|
|
10627
|
+
}
|
|
10628
|
+
if (Array.isArray(rulesConfig?.["header-max-length"]) && rulesConfig["header-max-length"].length >= 3) {
|
|
10629
|
+
const [, , maxLen] = rulesConfig["header-max-length"];
|
|
10630
|
+
if (typeof maxLen === "number") rules.headerMaxLength = maxLen;
|
|
10631
|
+
}
|
|
10632
|
+
if (Array.isArray(rulesConfig?.["subject-case"]) && rulesConfig["subject-case"].length >= 3) {
|
|
10633
|
+
const [, , val] = rulesConfig["subject-case"];
|
|
10634
|
+
if (val != null) rules.subjectCase = Array.isArray(val) ? val : [val];
|
|
10635
|
+
}
|
|
10636
|
+
return Object.keys(rules).length > 0 ? rules : null;
|
|
9467
10637
|
}
|
|
9468
|
-
function
|
|
9469
|
-
const
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
10638
|
+
function detectLocalCommitlintRules() {
|
|
10639
|
+
const cwd = process.cwd();
|
|
10640
|
+
const files = [
|
|
10641
|
+
".commitlintrc.json",
|
|
10642
|
+
".commitlintrc",
|
|
10643
|
+
"commitlint.config.js",
|
|
10644
|
+
"commitlint.config.cjs",
|
|
10645
|
+
"commitlint.config.mjs"
|
|
10646
|
+
];
|
|
10647
|
+
for (const file of files) {
|
|
10648
|
+
const path = (0, import_path10.join)(cwd, file);
|
|
10649
|
+
if (!(0, import_fs10.existsSync)(path)) continue;
|
|
10650
|
+
try {
|
|
10651
|
+
const content = (0, import_fs10.readFileSync)(path, "utf-8");
|
|
10652
|
+
let parsed;
|
|
10653
|
+
if (file.endsWith(".json") || file === ".commitlintrc") {
|
|
10654
|
+
parsed = JSON.parse(content);
|
|
10655
|
+
} else {
|
|
10656
|
+
continue;
|
|
10657
|
+
}
|
|
10658
|
+
const rules = mapCommitlintToRules(parsed);
|
|
10659
|
+
if (rules) return rules;
|
|
10660
|
+
} catch {
|
|
9474
10661
|
}
|
|
9475
|
-
process.stderr.write("\n");
|
|
9476
|
-
}
|
|
9477
|
-
}
|
|
9478
|
-
var import_promises;
|
|
9479
|
-
var init_commit_helpers = __esm({
|
|
9480
|
-
"src/commit-helpers.ts"() {
|
|
9481
|
-
"use strict";
|
|
9482
|
-
import_promises = __toESM(require("node:readline/promises"));
|
|
9483
10662
|
}
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
function getLegacyProvider() {
|
|
9493
|
-
try {
|
|
9494
|
-
const p = (0, import_path10.join)(CONFIG_PATH2, "provider");
|
|
9495
|
-
if ((0, import_fs10.existsSync)(p)) {
|
|
9496
|
-
const v = (0, import_fs10.readFileSync)(p, "utf-8").trim().toLowerCase();
|
|
9497
|
-
if (["ollama", "lmstudio", "openrouter", "custom", "cloudflare"].includes(v)) {
|
|
9498
|
-
return v;
|
|
10663
|
+
const pkgPath = (0, import_path10.join)(cwd, "package.json");
|
|
10664
|
+
if ((0, import_fs10.existsSync)(pkgPath)) {
|
|
10665
|
+
try {
|
|
10666
|
+
const content = (0, import_fs10.readFileSync)(pkgPath, "utf-8");
|
|
10667
|
+
const pkg = JSON.parse(content);
|
|
10668
|
+
if (pkg.commitlint) {
|
|
10669
|
+
const rules = mapCommitlintToRules(pkg.commitlint);
|
|
10670
|
+
if (rules) return rules;
|
|
9499
10671
|
}
|
|
10672
|
+
} catch {
|
|
9500
10673
|
}
|
|
9501
|
-
}
|
|
10674
|
+
}
|
|
10675
|
+
const config2 = getConfig();
|
|
10676
|
+
if (config2.rules && Object.keys(config2.rules).length > 0) {
|
|
10677
|
+
return config2.rules;
|
|
9502
10678
|
}
|
|
9503
10679
|
return null;
|
|
9504
10680
|
}
|
|
9505
|
-
function
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
10681
|
+
async function team(subcommand, args) {
|
|
10682
|
+
const api = createApiClient();
|
|
10683
|
+
switch (subcommand) {
|
|
10684
|
+
case void 0:
|
|
10685
|
+
case "info": {
|
|
10686
|
+
const info = await api.getTeam();
|
|
10687
|
+
console.log(`
|
|
10688
|
+
Team: ${info.name}`);
|
|
10689
|
+
console.log(` Plan: ${info.plan}`);
|
|
10690
|
+
console.log(` Members: ${info.member_count}`);
|
|
10691
|
+
console.log("\n Members:");
|
|
10692
|
+
for (const m of info.members) {
|
|
10693
|
+
console.log(` ${m.name ?? m.email} <${m.email}> (${m.role})`);
|
|
10694
|
+
}
|
|
10695
|
+
break;
|
|
9510
10696
|
}
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
10697
|
+
case "rules": {
|
|
10698
|
+
if (args?.[0] === "push") {
|
|
10699
|
+
const rules = detectLocalCommitlintRules();
|
|
10700
|
+
if (!rules) {
|
|
10701
|
+
console.error("No local commitlint config found.");
|
|
10702
|
+
process.exit(1);
|
|
10703
|
+
}
|
|
10704
|
+
await api.pushTeamRules(rules);
|
|
10705
|
+
console.log("Team rules updated from local commitlint config.");
|
|
10706
|
+
} else {
|
|
10707
|
+
const rules = await api.getTeamRules();
|
|
10708
|
+
console.log("\n Team Commit Rules:");
|
|
10709
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
10710
|
+
}
|
|
10711
|
+
break;
|
|
9521
10712
|
}
|
|
9522
|
-
|
|
10713
|
+
case "invite": {
|
|
10714
|
+
const email = args?.[0];
|
|
10715
|
+
if (!email) {
|
|
10716
|
+
console.error("Usage: qc team invite <email>");
|
|
10717
|
+
process.exit(1);
|
|
10718
|
+
}
|
|
10719
|
+
await api.inviteTeamMember(email);
|
|
10720
|
+
console.log(`Invitation sent to ${email}`);
|
|
10721
|
+
break;
|
|
10722
|
+
}
|
|
10723
|
+
default:
|
|
10724
|
+
console.error(`Unknown team command: ${subcommand}`);
|
|
10725
|
+
console.log("Usage: qc team [info|rules|rules push|invite <email>]");
|
|
10726
|
+
process.exit(1);
|
|
9523
10727
|
}
|
|
9524
|
-
return DEFAULT_MODELS[provider] ?? "";
|
|
9525
|
-
}
|
|
9526
|
-
function getLocalProviderConfig() {
|
|
9527
|
-
const config2 = getConfig();
|
|
9528
|
-
const provider = config2.provider ?? getLegacyProvider();
|
|
9529
|
-
if (!provider) return null;
|
|
9530
|
-
const baseUrl = config2.apiUrl ?? getLegacyBaseUrl(provider) ?? PROVIDER_URLS[provider] ?? "";
|
|
9531
|
-
if (!baseUrl) return null;
|
|
9532
|
-
const model = config2.model ?? getLegacyModel(provider) ?? DEFAULT_MODELS[provider];
|
|
9533
|
-
const apiKey = provider === "openrouter" || provider === "custom" ? getApiKey() : null;
|
|
9534
|
-
if (provider === "openrouter" && !apiKey) return null;
|
|
9535
|
-
return { provider, baseUrl, model, apiKey };
|
|
9536
10728
|
}
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
## Diff:
|
|
9546
|
-
<diff>
|
|
9547
|
-
${diff}
|
|
9548
|
-
</diff>
|
|
9549
|
-
|
|
9550
|
-
`;
|
|
9551
|
-
if (recentCommits && recentCommits.length > 0) {
|
|
9552
|
-
const history = recentCommits.slice(0, 10).join("\n");
|
|
9553
|
-
prompt2 += `Recent commits on this branch (match style when appropriate):
|
|
9554
|
-
${history}
|
|
9555
|
-
|
|
9556
|
-
`;
|
|
10729
|
+
var import_fs10, import_path10;
|
|
10730
|
+
var init_team = __esm({
|
|
10731
|
+
"src/commands/team.ts"() {
|
|
10732
|
+
"use strict";
|
|
10733
|
+
import_fs10 = require("fs");
|
|
10734
|
+
import_path10 = require("path");
|
|
10735
|
+
init_api();
|
|
10736
|
+
init_config();
|
|
9557
10737
|
}
|
|
9558
|
-
|
|
9559
|
-
prompt2 += `MULTI-COMMIT MODE: If changes span multiple logical commits, focus the message on the primary change and mention other slices in the body.
|
|
10738
|
+
});
|
|
9560
10739
|
|
|
9561
|
-
|
|
10740
|
+
// src/commands/config.ts
|
|
10741
|
+
var config_exports2 = {};
|
|
10742
|
+
__export(config_exports2, {
|
|
10743
|
+
config: () => config
|
|
10744
|
+
});
|
|
10745
|
+
function config(args) {
|
|
10746
|
+
if (args.length === 0) {
|
|
10747
|
+
showConfig();
|
|
10748
|
+
return;
|
|
9562
10749
|
}
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
10750
|
+
const sub = args[0];
|
|
10751
|
+
if (sub === "set") {
|
|
10752
|
+
const key = args[1];
|
|
10753
|
+
const value = args[2];
|
|
10754
|
+
if (!key || !value) {
|
|
10755
|
+
console.error("Usage: qc config set <key> <value>");
|
|
10756
|
+
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
10757
|
+
process.exit(1);
|
|
10758
|
+
}
|
|
10759
|
+
setConfig(key, value);
|
|
10760
|
+
return;
|
|
9567
10761
|
}
|
|
9568
|
-
if (
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
`;
|
|
10762
|
+
if (sub === "reset") {
|
|
10763
|
+
resetConfig();
|
|
10764
|
+
return;
|
|
9572
10765
|
}
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
return prompt2;
|
|
10766
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
10767
|
+
console.error("Usage: qc config [set <key> <value> | reset]");
|
|
10768
|
+
process.exit(1);
|
|
9577
10769
|
}
|
|
9578
|
-
function
|
|
9579
|
-
const
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
}
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
10770
|
+
function showConfig() {
|
|
10771
|
+
const cfg = getConfig();
|
|
10772
|
+
const apiKey = getApiKey();
|
|
10773
|
+
console.log("Current configuration:");
|
|
10774
|
+
console.log(` model: ${cfg.model ?? "(default for plan)"}`);
|
|
10775
|
+
console.log(` api_url: ${cfg.apiUrl ?? DEFAULT_API_URL}`);
|
|
10776
|
+
console.log(` provider: ${cfg.provider ?? "(default)"}`);
|
|
10777
|
+
console.log(` auto_stage: ${cfg.autoStage ? "true" : "false"}`);
|
|
10778
|
+
console.log(` auth: ${apiKey ? "****" : "not set"}`);
|
|
10779
|
+
if (cfg.excludes?.length) {
|
|
10780
|
+
console.log(` excludes: ${cfg.excludes.join(", ")}`);
|
|
9588
10781
|
}
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9601
|
-
case "lmstudio":
|
|
9602
|
-
url = `${baseUrl}/chat/completions`;
|
|
9603
|
-
body = {
|
|
9604
|
-
model,
|
|
9605
|
-
stream: false,
|
|
9606
|
-
messages: [
|
|
9607
|
-
{
|
|
9608
|
-
role: "system",
|
|
9609
|
-
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9610
|
-
},
|
|
9611
|
-
{ role: "user", content: userContent }
|
|
9612
|
-
]
|
|
9613
|
-
};
|
|
9614
|
-
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9615
|
-
case "openrouter":
|
|
9616
|
-
case "custom":
|
|
9617
|
-
url = `${baseUrl}/chat/completions`;
|
|
9618
|
-
body = {
|
|
9619
|
-
model,
|
|
9620
|
-
stream: false,
|
|
9621
|
-
messages: [
|
|
9622
|
-
{
|
|
9623
|
-
role: "system",
|
|
9624
|
-
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9625
|
-
},
|
|
9626
|
-
{ role: "user", content: userContent }
|
|
9627
|
-
]
|
|
9628
|
-
};
|
|
9629
|
-
return { url, body, headers };
|
|
9630
|
-
case "cloudflare": {
|
|
9631
|
-
url = `${baseUrl.replace(/\/$/, "")}/commit`;
|
|
9632
|
-
const payload = { diff, changes, rules };
|
|
9633
|
-
if (recentCommits && recentCommits.length > 0) {
|
|
9634
|
-
payload.recent_commits = recentCommits.slice(0, 10);
|
|
9635
|
-
}
|
|
9636
|
-
if (hints && Object.keys(hints).length > 0) {
|
|
9637
|
-
payload.generation_hints = hints;
|
|
9638
|
-
}
|
|
9639
|
-
body = payload;
|
|
9640
|
-
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
10782
|
+
}
|
|
10783
|
+
function setConfig(key, value) {
|
|
10784
|
+
const cfg = getConfig();
|
|
10785
|
+
const updates = {};
|
|
10786
|
+
if (key === "model") {
|
|
10787
|
+
updates.model = value;
|
|
10788
|
+
} else if (key === "provider") {
|
|
10789
|
+
const valid = ["ollama", "lmstudio", "openrouter", "custom", "cloudflare"];
|
|
10790
|
+
if (!valid.includes(value.toLowerCase())) {
|
|
10791
|
+
console.error(`Invalid provider. Must be one of: ${valid.join(", ")}`);
|
|
10792
|
+
process.exit(1);
|
|
9641
10793
|
}
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
return r.response ?? "";
|
|
9651
|
-
case "lmstudio":
|
|
9652
|
-
case "openrouter":
|
|
9653
|
-
case "custom": {
|
|
9654
|
-
const choices = r.choices;
|
|
9655
|
-
return choices?.[0]?.message?.content ?? "";
|
|
10794
|
+
updates.provider = value.toLowerCase();
|
|
10795
|
+
} else if (key === "api_url") {
|
|
10796
|
+
try {
|
|
10797
|
+
new URL(value);
|
|
10798
|
+
updates.apiUrl = value;
|
|
10799
|
+
} catch {
|
|
10800
|
+
console.error("Invalid URL:", value);
|
|
10801
|
+
process.exit(1);
|
|
9656
10802
|
}
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
10803
|
+
} else if (key === "auto_stage") {
|
|
10804
|
+
updates.autoStage = value === "true" || value === "1";
|
|
10805
|
+
} else {
|
|
10806
|
+
console.error(`Unknown key: ${key}`);
|
|
10807
|
+
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
10808
|
+
process.exit(1);
|
|
9661
10809
|
}
|
|
10810
|
+
saveConfig({ ...cfg, ...updates });
|
|
10811
|
+
console.log(`Set ${key} = ${value}`);
|
|
9662
10812
|
}
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
const local = getLocalProviderConfig();
|
|
9673
|
-
if (!local) {
|
|
9674
|
-
throw new Error(
|
|
9675
|
-
"No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
|
|
9676
|
-
);
|
|
10813
|
+
function resetConfig() {
|
|
10814
|
+
saveConfig({});
|
|
10815
|
+
console.log("Config reset to defaults.");
|
|
10816
|
+
}
|
|
10817
|
+
var init_config2 = __esm({
|
|
10818
|
+
"src/commands/config.ts"() {
|
|
10819
|
+
"use strict";
|
|
10820
|
+
init_config();
|
|
10821
|
+
init_dist();
|
|
9677
10822
|
}
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
10823
|
+
});
|
|
10824
|
+
|
|
10825
|
+
// src/commands/upgrade.ts
|
|
10826
|
+
var upgrade_exports = {};
|
|
10827
|
+
__export(upgrade_exports, {
|
|
10828
|
+
upgrade: () => upgrade
|
|
10829
|
+
});
|
|
10830
|
+
async function upgrade() {
|
|
10831
|
+
console.log(`
|
|
10832
|
+
Opening ${BILLING_URL}
|
|
10833
|
+
`);
|
|
10834
|
+
try {
|
|
10835
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
10836
|
+
if (process.platform === "darwin") {
|
|
10837
|
+
execFileSync7("open", [BILLING_URL]);
|
|
10838
|
+
} else if (process.platform === "linux") {
|
|
10839
|
+
execFileSync7("xdg-open", [BILLING_URL]);
|
|
10840
|
+
} else if (process.platform === "win32") {
|
|
10841
|
+
execFileSync7("cmd", ["/c", "start", "", BILLING_URL]);
|
|
9689
10842
|
}
|
|
10843
|
+
} catch {
|
|
10844
|
+
console.log(`Visit: ${BILLING_URL}`);
|
|
9690
10845
|
}
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
const scopes = scope.split(",").map((s) => s.trim());
|
|
9698
|
-
rules = { ...rules, scopes };
|
|
9699
|
-
}
|
|
10846
|
+
}
|
|
10847
|
+
var BILLING_URL;
|
|
10848
|
+
var init_upgrade = __esm({
|
|
10849
|
+
"src/commands/upgrade.ts"() {
|
|
10850
|
+
"use strict";
|
|
10851
|
+
BILLING_URL = "https://app.quikcommit.dev/billing";
|
|
9700
10852
|
}
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
rules,
|
|
9724
|
-
recentCommits,
|
|
9725
|
-
generationHints
|
|
10853
|
+
});
|
|
10854
|
+
|
|
10855
|
+
// src/branch-guard.ts
|
|
10856
|
+
async function runBranchGuard(args, log) {
|
|
10857
|
+
if (!shouldRunGuard({
|
|
10858
|
+
allowProtected: !!args.allowProtected,
|
|
10859
|
+
hookMode: !!args.hookMode,
|
|
10860
|
+
isTTY: !!process.stdin.isTTY
|
|
10861
|
+
})) {
|
|
10862
|
+
return { action: "continue" };
|
|
10863
|
+
}
|
|
10864
|
+
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
10865
|
+
const config2 = getConfig2();
|
|
10866
|
+
const state = detectProtectedBranchState({
|
|
10867
|
+
protectedBranches: config2.branch?.protectedBranches,
|
|
10868
|
+
detectDefault: config2.branch?.detectDefault
|
|
10869
|
+
});
|
|
10870
|
+
if (!state.isProtected) {
|
|
10871
|
+
return { action: "continue" };
|
|
10872
|
+
}
|
|
10873
|
+
log.error(
|
|
10874
|
+
`You're on ${state.branch} (a protected branch).` + (state.commitsAhead > 0 ? ` ${state.commitsAhead} commit(s) ahead of upstream.` : "")
|
|
9726
10875
|
);
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
10876
|
+
let action;
|
|
10877
|
+
let usedConfigDefault = false;
|
|
10878
|
+
if (args.autoBranch) {
|
|
10879
|
+
action = "branch";
|
|
10880
|
+
} else if (config2.branch?.defaultAction === "branch") {
|
|
10881
|
+
action = "branch";
|
|
10882
|
+
usedConfigDefault = true;
|
|
10883
|
+
} else if (config2.branch?.defaultAction === "continue") {
|
|
10884
|
+
action = "continue";
|
|
10885
|
+
usedConfigDefault = true;
|
|
10886
|
+
} else {
|
|
10887
|
+
action = await promptProtectedAction(state.mode);
|
|
9731
10888
|
}
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
const t0 = Date.now();
|
|
9735
|
-
let res;
|
|
9736
|
-
try {
|
|
9737
|
-
res = await fetch(url, {
|
|
9738
|
-
method: "POST",
|
|
9739
|
-
headers,
|
|
9740
|
-
body: JSON.stringify(body)
|
|
9741
|
-
});
|
|
9742
|
-
} finally {
|
|
9743
|
-
spinner.stop();
|
|
10889
|
+
if (action === "continue" && usedConfigDefault) {
|
|
10890
|
+
log.dim("(continuing on protected branch per config `branch.defaultAction`)");
|
|
9744
10891
|
}
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
throw new Error(`Provider error (${res.status}): ${text}`);
|
|
10892
|
+
if (action === "abort") {
|
|
10893
|
+
log.dim("aborted.");
|
|
10894
|
+
return { action: "abort" };
|
|
9749
10895
|
}
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
|
|
9753
|
-
if (!message) {
|
|
9754
|
-
throw new Error("Failed to generate commit message.");
|
|
10896
|
+
if (action === "continue") {
|
|
10897
|
+
return { action: "continue" };
|
|
9755
10898
|
}
|
|
9756
|
-
const
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
10899
|
+
const stagedDiff = state.mode === "uncommitted" ? getStagedDiff(args.excludes ?? []) : "";
|
|
10900
|
+
const stagedChanges = state.mode === "uncommitted" ? getStagedFiles() : "";
|
|
10901
|
+
const recentCommits = state.mode === "rescue" ? getRecentBranchCommits(state.commitsAhead) : void 0;
|
|
10902
|
+
const branchRules = args.branchRules ?? (config2.branch?.generation?.types && config2.branch.generation.types.length > 0 ? { types: [...config2.branch.generation.types] } : void 0);
|
|
10903
|
+
const apiKey = args.apiKey;
|
|
10904
|
+
const ui2 = getUI();
|
|
10905
|
+
let generateLocalBranchNameFn;
|
|
10906
|
+
if (!apiKey) {
|
|
10907
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, generateLocalBranchName: generateLocalBranchName2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10908
|
+
if (!getLocalProviderConfig2()) {
|
|
10909
|
+
log.error(
|
|
10910
|
+
"Cannot generate branch name: not authenticated and no local provider configured. Run `qc login` or configure a local provider."
|
|
10911
|
+
);
|
|
10912
|
+
return { action: "abort" };
|
|
10913
|
+
}
|
|
10914
|
+
generateLocalBranchNameFn = generateLocalBranchName2;
|
|
10915
|
+
}
|
|
10916
|
+
const spinner = ui2.spinner(`generating branch name...`);
|
|
10917
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10918
|
+
let rawName;
|
|
10919
|
+
let usedFallback = false;
|
|
10920
|
+
try {
|
|
10921
|
+
if (apiKey) {
|
|
10922
|
+
const client = new ApiClient({ apiKey });
|
|
10923
|
+
try {
|
|
10924
|
+
const branchResult = await client.generateBranchName({
|
|
10925
|
+
diff: stagedDiff || void 0,
|
|
10926
|
+
changes: stagedChanges || void 0,
|
|
10927
|
+
recent_commits: recentCommits,
|
|
10928
|
+
model: args.model,
|
|
10929
|
+
rules: branchRules
|
|
10930
|
+
});
|
|
10931
|
+
rawName = branchResult.name;
|
|
10932
|
+
} catch {
|
|
10933
|
+
const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
|
|
10934
|
+
rawName = deterministicBranchName(fallbackInput).name;
|
|
10935
|
+
usedFallback = true;
|
|
10936
|
+
}
|
|
9761
10937
|
} else {
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
10938
|
+
try {
|
|
10939
|
+
rawName = await generateLocalBranchNameFn({
|
|
10940
|
+
diff: stagedDiff || void 0,
|
|
10941
|
+
changes: stagedChanges || void 0,
|
|
10942
|
+
recentCommits,
|
|
10943
|
+
model: args.model,
|
|
10944
|
+
rules: branchRules
|
|
10945
|
+
});
|
|
10946
|
+
} catch {
|
|
10947
|
+
const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
|
|
10948
|
+
rawName = deterministicBranchName(fallbackInput).name;
|
|
10949
|
+
usedFallback = true;
|
|
9765
10950
|
}
|
|
9766
|
-
message = refineResult.message;
|
|
9767
10951
|
}
|
|
10952
|
+
} finally {
|
|
10953
|
+
spinner.stop();
|
|
9768
10954
|
}
|
|
9769
|
-
if (
|
|
9770
|
-
|
|
9771
|
-
return;
|
|
9772
|
-
}
|
|
9773
|
-
if (!silent) {
|
|
9774
|
-
displayCommitMessage(message, log);
|
|
9775
|
-
}
|
|
9776
|
-
if (args.dryRun) {
|
|
9777
|
-
return;
|
|
10955
|
+
if (usedFallback) {
|
|
10956
|
+
log.dim("(used local fallback name; AI generation failed)");
|
|
9778
10957
|
}
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
10958
|
+
let final;
|
|
10959
|
+
try {
|
|
10960
|
+
final = finalizeBranchName(rawName, branchExists);
|
|
10961
|
+
} catch {
|
|
10962
|
+
const generatorName = usedFallback ? "deterministic fallback" : apiKey ? "API generator" : "local provider";
|
|
10963
|
+
log.error(`Invalid branch name from ${generatorName}: ${rawName}`);
|
|
10964
|
+
return { action: "abort" };
|
|
10965
|
+
}
|
|
10966
|
+
log.success(`branch name: ${final}`);
|
|
10967
|
+
if (state.mode === "rescue") {
|
|
10968
|
+
log.dim(
|
|
10969
|
+
`About to: 1) create ${final} at HEAD, 2) reset ${state.branch} to upstream, 3) switch to ${final}`
|
|
10970
|
+
);
|
|
10971
|
+
const confirmed = await promptYesNo("Continue with rescue?");
|
|
10972
|
+
if (!confirmed) {
|
|
10973
|
+
log.dim("aborted.");
|
|
10974
|
+
return { action: "abort" };
|
|
10975
|
+
}
|
|
10976
|
+
try {
|
|
10977
|
+
rescueCommits({ currentBranch: state.branch, newBranch: final });
|
|
10978
|
+
log.success(`moved ${state.commitsAhead} commit(s) to ${final}`);
|
|
10979
|
+
log.success(`${state.branch} reset to upstream`);
|
|
10980
|
+
} catch (err) {
|
|
10981
|
+
log.error(`Rescue failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10982
|
+
return { action: "abort" };
|
|
9783
10983
|
}
|
|
10984
|
+
return { action: "done" };
|
|
9784
10985
|
}
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
10986
|
+
createAndCheckoutBranch(final);
|
|
10987
|
+
log.success(`switched to ${final}`);
|
|
10988
|
+
return { action: "continue" };
|
|
10989
|
+
}
|
|
10990
|
+
async function promptProtectedAction(mode) {
|
|
10991
|
+
const rl = import_promises2.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
10992
|
+
try {
|
|
10993
|
+
const question = mode === "rescue" ? "Move commits to a new branch? [B/c/a] " : "Create a new branch first? [B/c/a] ";
|
|
10994
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
10995
|
+
if (answer === "" || answer === "b" || answer === "y") return "branch";
|
|
10996
|
+
if (answer === "c") return "continue";
|
|
10997
|
+
return "abort";
|
|
10998
|
+
} finally {
|
|
10999
|
+
rl.close();
|
|
9788
11000
|
}
|
|
9789
11001
|
}
|
|
9790
|
-
var
|
|
9791
|
-
var
|
|
9792
|
-
"src/
|
|
11002
|
+
var import_promises2;
|
|
11003
|
+
var init_branch_guard = __esm({
|
|
11004
|
+
"src/branch-guard.ts"() {
|
|
9793
11005
|
"use strict";
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
import_os4 = require("os");
|
|
9797
|
-
init_config();
|
|
9798
|
-
init_dist();
|
|
11006
|
+
import_promises2 = __toESM(require("node:readline/promises"));
|
|
11007
|
+
init_api();
|
|
9799
11008
|
init_git();
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
11009
|
+
init_protected_branch_guard();
|
|
11010
|
+
init_branch_rescue();
|
|
11011
|
+
init_branch_name();
|
|
9803
11012
|
init_ui();
|
|
9804
11013
|
init_commit_helpers();
|
|
9805
|
-
CONFIG_PATH2 = (0, import_path10.join)((0, import_os4.homedir)(), CONFIG_DIR);
|
|
9806
|
-
PROVIDER_URLS = {
|
|
9807
|
-
ollama: "http://localhost:11434",
|
|
9808
|
-
lmstudio: "http://localhost:1234/v1",
|
|
9809
|
-
openrouter: "https://openrouter.ai/api/v1",
|
|
9810
|
-
custom: "",
|
|
9811
|
-
cloudflare: ""
|
|
9812
|
-
};
|
|
9813
|
-
DEFAULT_MODELS = {
|
|
9814
|
-
ollama: "codellama",
|
|
9815
|
-
lmstudio: "default",
|
|
9816
|
-
openrouter: "google/gemini-flash-1.5-8b",
|
|
9817
|
-
custom: "",
|
|
9818
|
-
cloudflare: "@cf/qwen/qwen2.5-coder-32b-instruct"
|
|
9819
|
-
};
|
|
9820
11014
|
}
|
|
9821
11015
|
});
|
|
9822
11016
|
|
|
@@ -9828,12 +11022,33 @@ __export(commit_exports, {
|
|
|
9828
11022
|
async function runCommit(args) {
|
|
9829
11023
|
const { messageOnly, push, apiKey: apiKeyFlag, hookMode, model: modelFlag, all } = args;
|
|
9830
11024
|
const silent = !!(hookMode || args.quiet);
|
|
9831
|
-
const
|
|
11025
|
+
const ui2 = getUI();
|
|
11026
|
+
const log = silent ? createSilentLog() : ui2.log;
|
|
9832
11027
|
if (!isGitRepo()) {
|
|
9833
11028
|
log.error("Not a git repository.");
|
|
9834
11029
|
process.exit(1);
|
|
9835
11030
|
}
|
|
9836
11031
|
const config2 = getConfig();
|
|
11032
|
+
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
11033
|
+
const guardResult = await runBranchGuard(
|
|
11034
|
+
{
|
|
11035
|
+
allowProtected: !!(args.allowProtected || config2.branch?.allowProtected),
|
|
11036
|
+
autoBranch: !!args.autoBranch,
|
|
11037
|
+
hookMode: !!args.hookMode,
|
|
11038
|
+
apiKey: apiKeyFlag ?? getApiKey() ?? void 0,
|
|
11039
|
+
model: args.model,
|
|
11040
|
+
excludes
|
|
11041
|
+
},
|
|
11042
|
+
log
|
|
11043
|
+
);
|
|
11044
|
+
if (guardResult.action === "abort") {
|
|
11045
|
+
return;
|
|
11046
|
+
}
|
|
11047
|
+
if (guardResult.action === "done") {
|
|
11048
|
+
return;
|
|
11049
|
+
}
|
|
11050
|
+
const _exhaustive = guardResult.action;
|
|
11051
|
+
void _exhaustive;
|
|
9837
11052
|
if (all || config2.autoStage) {
|
|
9838
11053
|
stageAll();
|
|
9839
11054
|
const { files, total } = getShortStagedFiles();
|
|
@@ -9856,18 +11071,22 @@ async function runCommit(args) {
|
|
|
9856
11071
|
process.exit(1);
|
|
9857
11072
|
}
|
|
9858
11073
|
const model = modelFlag ?? config2.model;
|
|
9859
|
-
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
9860
11074
|
const diff = getStagedDiff(excludes);
|
|
9861
11075
|
const changes = getStagedFiles();
|
|
9862
11076
|
let processedDiff = diff;
|
|
9863
11077
|
if (!args.noSmartDiff) {
|
|
9864
|
-
const smartResult =
|
|
11078
|
+
const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
|
|
9865
11079
|
processedDiff = smartResult.processedDiff;
|
|
9866
11080
|
if (smartResult.summarized.length > 0) {
|
|
9867
11081
|
log.step(
|
|
9868
11082
|
`smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
|
|
9869
11083
|
);
|
|
9870
11084
|
}
|
|
11085
|
+
if (smartResult.aggressivelySummarized.length > 0) {
|
|
11086
|
+
log.step(
|
|
11087
|
+
`large-diff: ${smartResult.aggressivelySummarized.length} additional file(s) summarized to fit (commit message may be less specific \u2014 consider committing fewer files at a time)`
|
|
11088
|
+
);
|
|
11089
|
+
}
|
|
9871
11090
|
}
|
|
9872
11091
|
const commitlintRules = await detectCommitlintRules();
|
|
9873
11092
|
let rules = { ...commitlintRules, ...config2.rules ?? {} };
|
|
@@ -9901,7 +11120,7 @@ async function runCommit(args) {
|
|
|
9901
11120
|
const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9902
11121
|
const skipConfirm = args.dryRun || messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9903
11122
|
const modelDisplay = model ?? "default";
|
|
9904
|
-
const spinner =
|
|
11123
|
+
const spinner = ui2.spinner(`generating commit (${modelDisplay})...`);
|
|
9905
11124
|
if (!silent) spinner.start();
|
|
9906
11125
|
const t0 = Date.now();
|
|
9907
11126
|
let generatedMessage;
|
|
@@ -9927,7 +11146,7 @@ async function runCommit(args) {
|
|
|
9927
11146
|
} else {
|
|
9928
11147
|
const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
|
|
9929
11148
|
if (refineResult.action === "abort") {
|
|
9930
|
-
|
|
11149
|
+
return;
|
|
9931
11150
|
}
|
|
9932
11151
|
message = refineResult.message;
|
|
9933
11152
|
}
|
|
@@ -9936,14 +11155,29 @@ async function runCommit(args) {
|
|
|
9936
11155
|
console.log(message);
|
|
9937
11156
|
return;
|
|
9938
11157
|
}
|
|
9939
|
-
|
|
11158
|
+
const stagedPaths = changes.trim().split("\n").filter(Boolean);
|
|
11159
|
+
const short = getStagedDiffShortstat();
|
|
11160
|
+
const tokenEst = diagnostics && typeof diagnostics === "object" && diagnostics !== null && "tokenUsage" in diagnostics ? diagnostics.tokenUsage?.totalEstimated : void 0;
|
|
11161
|
+
displayCommitMessage(message, {
|
|
11162
|
+
log,
|
|
11163
|
+
isColor: ui2.isColor,
|
|
11164
|
+
isTTY: !!process.stderr.isTTY,
|
|
11165
|
+
style: "rich",
|
|
11166
|
+
stagedFiles: stagedPaths,
|
|
11167
|
+
stats: {
|
|
11168
|
+
files: stagedPaths.length,
|
|
11169
|
+
additions: short.additions,
|
|
11170
|
+
deletions: short.deletions,
|
|
11171
|
+
...tokenEst !== void 0 ? { tokens: tokenEst } : {}
|
|
11172
|
+
}
|
|
11173
|
+
});
|
|
9940
11174
|
if (args.dryRun) {
|
|
9941
11175
|
return;
|
|
9942
11176
|
}
|
|
9943
11177
|
if (args.confirm) {
|
|
9944
11178
|
const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
|
|
9945
11179
|
if (confirmResult.action === "abort") {
|
|
9946
|
-
|
|
11180
|
+
return;
|
|
9947
11181
|
}
|
|
9948
11182
|
}
|
|
9949
11183
|
gitCommit(message);
|
|
@@ -9972,6 +11206,7 @@ var init_commit = __esm({
|
|
|
9972
11206
|
init_ui();
|
|
9973
11207
|
init_smart_diff();
|
|
9974
11208
|
init_commit_helpers();
|
|
11209
|
+
init_branch_guard();
|
|
9975
11210
|
}
|
|
9976
11211
|
});
|
|
9977
11212
|
|
|
@@ -9989,6 +11224,7 @@ Usage:
|
|
|
9989
11224
|
qc pr Generate PR description from branch commits
|
|
9990
11225
|
qc changelog Generate changelog from commits since last tag
|
|
9991
11226
|
qc changeset Automate pnpm changeset with AI
|
|
11227
|
+
qc branch Generate branch name + create branch (use --message for description)
|
|
9992
11228
|
qc init Install prepare-commit-msg hook
|
|
9993
11229
|
qc login Sign in via browser
|
|
9994
11230
|
qc logout Clear local credentials
|
|
@@ -10018,11 +11254,22 @@ Flags:
|
|
|
10018
11254
|
--model <id> Use specific model
|
|
10019
11255
|
--base <branch> Base branch for pr/changeset (default: main)
|
|
10020
11256
|
--create Create PR with gh CLI (qc pr --create)
|
|
10021
|
-
--from <ref> Start ref for changelog
|
|
11257
|
+
--from <ref> Start ref for changelog / base ref for qc branch
|
|
10022
11258
|
--to <ref> End ref for changelog
|
|
10023
11259
|
--write Write changelog to CHANGELOG.md
|
|
10024
11260
|
--hook-mode Silent mode for git hooks
|
|
10025
11261
|
|
|
11262
|
+
Branch flags (qc branch):
|
|
11263
|
+
--message <text> Generate from a description (no diff needed)
|
|
11264
|
+
--from-commits Generate from recent commits instead of diff
|
|
11265
|
+
--rescue Move commits off current protected branch (see docs)
|
|
11266
|
+
--no-switch Create branch but don't checkout
|
|
11267
|
+
--from <ref> Create branch from this ref (default: HEAD)
|
|
11268
|
+
|
|
11269
|
+
Commit guard flags:
|
|
11270
|
+
--allow-protected Bypass protected-branch guard for this run
|
|
11271
|
+
--auto-branch Auto-create branch with generated name (no prompt)
|
|
11272
|
+
|
|
10026
11273
|
Compose short flags: qc -ap (stage all + push), qc -apv (+ verbose)
|
|
10027
11274
|
|
|
10028
11275
|
Examples:
|
|
@@ -10133,10 +11380,18 @@ function parseArgs(args) {
|
|
|
10133
11380
|
result.command = "help";
|
|
10134
11381
|
} else if (arg === "--all") {
|
|
10135
11382
|
result.all = true;
|
|
11383
|
+
} else if (arg === "--allow-protected") {
|
|
11384
|
+
result.allowProtected = true;
|
|
11385
|
+
} else if (arg === "--auto-branch") {
|
|
11386
|
+
result.autoBranch = true;
|
|
10136
11387
|
} else if (arg === "--message-only") {
|
|
10137
11388
|
result.messageOnly = true;
|
|
11389
|
+
} else if (arg === "--message" && i + 1 < args.length) {
|
|
11390
|
+
result.message = args[++i];
|
|
10138
11391
|
} else if (arg === "--push") {
|
|
10139
11392
|
result.push = true;
|
|
11393
|
+
} else if (arg === "--rescue") {
|
|
11394
|
+
result.rescue = true;
|
|
10140
11395
|
} else if (arg === "--verbose") {
|
|
10141
11396
|
result.verbose = true;
|
|
10142
11397
|
} else if (arg === "--quiet") {
|
|
@@ -10157,6 +11412,8 @@ function parseArgs(args) {
|
|
|
10157
11412
|
result.noContext = true;
|
|
10158
11413
|
} else if (arg === "--no-smart-diff") {
|
|
10159
11414
|
result.noSmartDiff = true;
|
|
11415
|
+
} else if (arg === "--no-switch") {
|
|
11416
|
+
result.noSwitch = true;
|
|
10160
11417
|
} else if (arg === "--local" || arg === "--use-ollama" || arg === "--use-lmstudio" || arg === "--use-openrouter" || arg === "--use-cloudflare") {
|
|
10161
11418
|
result.local = true;
|
|
10162
11419
|
if (arg === "--use-ollama") {
|
|
@@ -10176,6 +11433,8 @@ function parseArgs(args) {
|
|
|
10176
11433
|
result.create = true;
|
|
10177
11434
|
} else if (arg === "--from" && i + 1 < args.length) {
|
|
10178
11435
|
result.from = args[++i];
|
|
11436
|
+
} else if (arg === "--from-commits") {
|
|
11437
|
+
result.fromCommits = true;
|
|
10179
11438
|
} else if (arg === "--to" && i + 1 < args.length) {
|
|
10180
11439
|
result.to = args[++i];
|
|
10181
11440
|
} else if (arg === "--write") {
|
|
@@ -10211,6 +11470,9 @@ function parseArgs(args) {
|
|
|
10211
11470
|
} else if (arg === "changelog") {
|
|
10212
11471
|
result.command = "changelog";
|
|
10213
11472
|
subcommandSeen = true;
|
|
11473
|
+
} else if (arg === "branch") {
|
|
11474
|
+
result.command = "branch";
|
|
11475
|
+
subcommandSeen = true;
|
|
10214
11476
|
} else if (arg === "init") {
|
|
10215
11477
|
result.command = "init";
|
|
10216
11478
|
subcommandSeen = true;
|
|
@@ -10325,6 +11587,23 @@ async function main() {
|
|
|
10325
11587
|
});
|
|
10326
11588
|
return;
|
|
10327
11589
|
}
|
|
11590
|
+
if (command === "branch") {
|
|
11591
|
+
const { runBranch: runBranch2 } = await Promise.resolve().then(() => (init_branch2(), branch_exports));
|
|
11592
|
+
const explicitName = values.positionals[0];
|
|
11593
|
+
await runBranch2({
|
|
11594
|
+
explicitName,
|
|
11595
|
+
message: values.message,
|
|
11596
|
+
fromCommits: values.fromCommits,
|
|
11597
|
+
rescue: values.rescue,
|
|
11598
|
+
dryRun: values.dryRun,
|
|
11599
|
+
noSwitch: values.noSwitch,
|
|
11600
|
+
push: values.push,
|
|
11601
|
+
from: values.from,
|
|
11602
|
+
model: values.model,
|
|
11603
|
+
apiKey: values.apiKey
|
|
11604
|
+
});
|
|
11605
|
+
return;
|
|
11606
|
+
}
|
|
10328
11607
|
if (command === "init") {
|
|
10329
11608
|
const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
10330
11609
|
init2({ uninstall: values.uninstall });
|
|
@@ -10336,7 +11615,7 @@ async function main() {
|
|
|
10336
11615
|
return;
|
|
10337
11616
|
}
|
|
10338
11617
|
if (command === "config") {
|
|
10339
|
-
const { config: config2 } = await Promise.resolve().then(() => (init_config2(),
|
|
11618
|
+
const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
10340
11619
|
config2(values.positionals);
|
|
10341
11620
|
return;
|
|
10342
11621
|
}
|