@quikcommit/cli 8.0.0 → 10.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 +2191 -850
- 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
|
}
|
|
@@ -7793,6 +7879,26 @@ function getStagedFiles() {
|
|
|
7793
7879
|
encoding: "utf-8"
|
|
7794
7880
|
});
|
|
7795
7881
|
}
|
|
7882
|
+
function getWorkingTreeDiff(excludes = []) {
|
|
7883
|
+
const args = ["diff", "HEAD"];
|
|
7884
|
+
if (excludes.length > 0) {
|
|
7885
|
+
args.push("--");
|
|
7886
|
+
args.push(".");
|
|
7887
|
+
for (const pattern of excludes) {
|
|
7888
|
+
args.push(`:(exclude)${pattern}`);
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
return (0, import_child_process2.execFileSync)("git", args, {
|
|
7892
|
+
encoding: "utf-8",
|
|
7893
|
+
maxBuffer: 10 * 1024 * 1024
|
|
7894
|
+
});
|
|
7895
|
+
}
|
|
7896
|
+
function getAllChangedFiles() {
|
|
7897
|
+
const output = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], {
|
|
7898
|
+
encoding: "utf-8"
|
|
7899
|
+
});
|
|
7900
|
+
return output.trim().split("\n").filter(Boolean).map((line) => line.slice(3)).join("\n");
|
|
7901
|
+
}
|
|
7796
7902
|
function hasStagedChanges() {
|
|
7797
7903
|
const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
|
|
7798
7904
|
encoding: "utf-8"
|
|
@@ -7803,17 +7909,21 @@ function getUnstagedFiles() {
|
|
|
7803
7909
|
const output = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], {
|
|
7804
7910
|
encoding: "utf-8"
|
|
7805
7911
|
});
|
|
7806
|
-
return output.trim().split("\n").filter(Boolean)
|
|
7912
|
+
return output.trim().split("\n").filter(Boolean);
|
|
7807
7913
|
}
|
|
7808
7914
|
function stageAll() {
|
|
7809
|
-
(0, import_child_process2.execFileSync)("git", ["add", "-
|
|
7915
|
+
(0, import_child_process2.execFileSync)("git", ["add", "-A"], { stdio: "pipe" });
|
|
7810
7916
|
}
|
|
7811
7917
|
function gitCommit(message) {
|
|
7812
7918
|
const tmpDir = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os3.tmpdir)(), "qc-"));
|
|
7813
7919
|
const tmpFile = (0, import_path2.join)(tmpDir, "commit.txt");
|
|
7814
7920
|
(0, import_fs2.writeFileSync)(tmpFile, message, { mode: 384 });
|
|
7815
7921
|
try {
|
|
7816
|
-
(0, import_child_process2.execFileSync)("git", ["commit", "-F", tmpFile], { stdio: "
|
|
7922
|
+
(0, import_child_process2.execFileSync)("git", ["commit", "-F", tmpFile], { stdio: "pipe" });
|
|
7923
|
+
} catch (err) {
|
|
7924
|
+
const stderr = err?.stderr?.toString() ?? "";
|
|
7925
|
+
if (stderr) process.stderr.write(stderr);
|
|
7926
|
+
throw err;
|
|
7817
7927
|
} finally {
|
|
7818
7928
|
try {
|
|
7819
7929
|
(0, import_fs2.unlinkSync)(tmpFile);
|
|
@@ -7823,7 +7933,13 @@ function gitCommit(message) {
|
|
|
7823
7933
|
}
|
|
7824
7934
|
}
|
|
7825
7935
|
function gitPush() {
|
|
7826
|
-
|
|
7936
|
+
try {
|
|
7937
|
+
(0, import_child_process2.execFileSync)("git", ["push"], { stdio: "pipe" });
|
|
7938
|
+
} catch (err) {
|
|
7939
|
+
const stderr = err?.stderr?.toString() ?? "";
|
|
7940
|
+
if (stderr) process.stderr.write(stderr);
|
|
7941
|
+
throw err;
|
|
7942
|
+
}
|
|
7827
7943
|
}
|
|
7828
7944
|
function getBranchCommits(base = "main") {
|
|
7829
7945
|
validateRef(base, "base");
|
|
@@ -7893,12 +8009,28 @@ function getFullDiff(base = "main") {
|
|
|
7893
8009
|
maxBuffer: 10 * 1024 * 1024
|
|
7894
8010
|
});
|
|
7895
8011
|
}
|
|
7896
|
-
function
|
|
8012
|
+
function getStagedFileCount() {
|
|
7897
8013
|
const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
|
|
7898
8014
|
encoding: "utf-8"
|
|
7899
8015
|
});
|
|
7900
|
-
|
|
7901
|
-
|
|
8016
|
+
return output.trim().split("\n").filter(Boolean).length;
|
|
8017
|
+
}
|
|
8018
|
+
function getStagedDiffShortstat() {
|
|
8019
|
+
try {
|
|
8020
|
+
const out = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--shortstat"], {
|
|
8021
|
+
encoding: "utf-8"
|
|
8022
|
+
}).trim();
|
|
8023
|
+
if (!out) return { additions: 0, deletions: 0 };
|
|
8024
|
+
let additions = 0;
|
|
8025
|
+
let deletions = 0;
|
|
8026
|
+
const ins = /(\d+) insertion/.exec(out);
|
|
8027
|
+
const del = /(\d+) deletion/.exec(out);
|
|
8028
|
+
if (ins?.[1]) additions = parseInt(ins[1], 10);
|
|
8029
|
+
if (del?.[1]) deletions = parseInt(del[1], 10);
|
|
8030
|
+
return { additions, deletions };
|
|
8031
|
+
} catch {
|
|
8032
|
+
return { additions: 0, deletions: 0 };
|
|
8033
|
+
}
|
|
7902
8034
|
}
|
|
7903
8035
|
function getCommitHash() {
|
|
7904
8036
|
return (0, import_child_process2.execFileSync)("git", ["rev-parse", "--short", "HEAD"], {
|
|
@@ -7939,6 +8071,111 @@ function getRecentBranchCommits(count = 5) {
|
|
|
7939
8071
|
return [];
|
|
7940
8072
|
}
|
|
7941
8073
|
}
|
|
8074
|
+
function getCommitsAheadOfUpstream(branch, upstream) {
|
|
8075
|
+
validateRef(branch, "branch");
|
|
8076
|
+
const target = upstream ?? `origin/${branch}`;
|
|
8077
|
+
validateRef(target, "upstream");
|
|
8078
|
+
try {
|
|
8079
|
+
const out = (0, import_child_process2.execFileSync)(
|
|
8080
|
+
"git",
|
|
8081
|
+
["rev-list", "--count", `${target}..HEAD`],
|
|
8082
|
+
{ encoding: "utf-8" }
|
|
8083
|
+
).trim();
|
|
8084
|
+
const n = parseInt(out, 10);
|
|
8085
|
+
return Number.isFinite(n) ? n : 0;
|
|
8086
|
+
} catch {
|
|
8087
|
+
return 0;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
function getUpstreamRef(branch) {
|
|
8091
|
+
validateRef(branch, "branch");
|
|
8092
|
+
try {
|
|
8093
|
+
return (0, import_child_process2.execFileSync)(
|
|
8094
|
+
"git",
|
|
8095
|
+
["rev-parse", "--abbrev-ref", `${branch}@{upstream}`],
|
|
8096
|
+
{ encoding: "utf-8" }
|
|
8097
|
+
).trim() || null;
|
|
8098
|
+
} catch {
|
|
8099
|
+
return null;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
function getDefaultBranch() {
|
|
8103
|
+
try {
|
|
8104
|
+
const out = (0, import_child_process2.execFileSync)(
|
|
8105
|
+
"git",
|
|
8106
|
+
["symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
8107
|
+
{ encoding: "utf-8" }
|
|
8108
|
+
).trim();
|
|
8109
|
+
const segments = out.split("/");
|
|
8110
|
+
return segments[segments.length - 1] || null;
|
|
8111
|
+
} catch {
|
|
8112
|
+
return null;
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
function branchExists(name) {
|
|
8116
|
+
validateRef(name, "branch");
|
|
8117
|
+
try {
|
|
8118
|
+
(0, import_child_process2.execFileSync)("git", ["show-ref", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
8119
|
+
stdio: "pipe"
|
|
8120
|
+
});
|
|
8121
|
+
return true;
|
|
8122
|
+
} catch {
|
|
8123
|
+
}
|
|
8124
|
+
try {
|
|
8125
|
+
(0, import_child_process2.execFileSync)("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${name}`], {
|
|
8126
|
+
stdio: "pipe"
|
|
8127
|
+
});
|
|
8128
|
+
return true;
|
|
8129
|
+
} catch {
|
|
8130
|
+
return false;
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
function stashPushIfDirty(message) {
|
|
8134
|
+
const status = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], { encoding: "utf-8" }).trim();
|
|
8135
|
+
if (!status) return false;
|
|
8136
|
+
(0, import_child_process2.execFileSync)("git", ["stash", "push", "--include-untracked", "--message", message], {
|
|
8137
|
+
stdio: "pipe"
|
|
8138
|
+
});
|
|
8139
|
+
return true;
|
|
8140
|
+
}
|
|
8141
|
+
function stashPop() {
|
|
8142
|
+
(0, import_child_process2.execFileSync)("git", ["stash", "pop"], { stdio: "pipe" });
|
|
8143
|
+
}
|
|
8144
|
+
function resetHard(ref) {
|
|
8145
|
+
validateRef(ref, "ref");
|
|
8146
|
+
(0, import_child_process2.execFileSync)("git", ["reset", "--hard", ref], { stdio: "pipe" });
|
|
8147
|
+
}
|
|
8148
|
+
function createBranch(name, base = "HEAD") {
|
|
8149
|
+
validateRef(name, "name");
|
|
8150
|
+
validateRef(base, "base");
|
|
8151
|
+
(0, import_child_process2.execFileSync)("git", ["branch", name, base], { stdio: "pipe" });
|
|
8152
|
+
}
|
|
8153
|
+
function checkoutBranch(name) {
|
|
8154
|
+
validateRef(name, "name");
|
|
8155
|
+
(0, import_child_process2.execFileSync)("git", ["checkout", name], { stdio: "pipe" });
|
|
8156
|
+
}
|
|
8157
|
+
function createAndCheckoutBranch(name, base = "HEAD") {
|
|
8158
|
+
validateRef(name, "name");
|
|
8159
|
+
validateRef(base, "base");
|
|
8160
|
+
(0, import_child_process2.execFileSync)("git", ["checkout", "-b", name, base], { stdio: "pipe" });
|
|
8161
|
+
}
|
|
8162
|
+
function getHeadSha() {
|
|
8163
|
+
return (0, import_child_process2.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).trim();
|
|
8164
|
+
}
|
|
8165
|
+
function gitPushSetUpstream(branch) {
|
|
8166
|
+
validateRef(branch, "branch");
|
|
8167
|
+
try {
|
|
8168
|
+
(0, import_child_process2.execFileSync)("git", ["push", "-u", "origin", branch], { stdio: "pipe" });
|
|
8169
|
+
} catch (err) {
|
|
8170
|
+
const stderr = err?.stderr?.toString() ?? "";
|
|
8171
|
+
if (stderr) process.stderr.write(stderr);
|
|
8172
|
+
throw err;
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
function deleteBranch(name) {
|
|
8176
|
+
validateRef(name, "name");
|
|
8177
|
+
(0, import_child_process2.execFileSync)("git", ["branch", "-D", name], { stdio: "pipe" });
|
|
8178
|
+
}
|
|
7942
8179
|
var import_child_process2, import_fs2, import_path2, import_os3, SAFE_GIT_REF;
|
|
7943
8180
|
var init_git = __esm({
|
|
7944
8181
|
"src/git.ts"() {
|
|
@@ -7947,7 +8184,7 @@ var init_git = __esm({
|
|
|
7947
8184
|
import_fs2 = require("fs");
|
|
7948
8185
|
import_path2 = require("path");
|
|
7949
8186
|
import_os3 = require("os");
|
|
7950
|
-
SAFE_GIT_REF = /^[a-zA-Z0-9._\-/~:^@]
|
|
8187
|
+
SAFE_GIT_REF = /^[a-zA-Z0-9][a-zA-Z0-9._\-/~:^@]*$/;
|
|
7951
8188
|
}
|
|
7952
8189
|
});
|
|
7953
8190
|
|
|
@@ -8762,336 +8999,625 @@ var init_changeset = __esm({
|
|
|
8762
8999
|
}
|
|
8763
9000
|
});
|
|
8764
9001
|
|
|
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);
|
|
9002
|
+
// src/branch-rescue.ts
|
|
9003
|
+
function rescueCommits(opts) {
|
|
9004
|
+
const upstream = getUpstreamRef(opts.currentBranch);
|
|
9005
|
+
if (!upstream) {
|
|
9006
|
+
throw new Error(
|
|
9007
|
+
`No upstream tracking branch for '${opts.currentBranch}'. Push it first or use \`qc branch\` manually.`
|
|
9008
|
+
);
|
|
8779
9009
|
}
|
|
8780
|
-
const
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
9010
|
+
const headSha = getHeadSha();
|
|
9011
|
+
const stashed = stashPushIfDirty(`qc-rescue-${opts.newBranch}`);
|
|
9012
|
+
try {
|
|
9013
|
+
createBranch(opts.newBranch, headSha);
|
|
9014
|
+
} catch (err) {
|
|
9015
|
+
if (stashed) {
|
|
9016
|
+
try {
|
|
9017
|
+
stashPop();
|
|
9018
|
+
} catch {
|
|
8789
9019
|
}
|
|
8790
|
-
} else {
|
|
8791
|
-
console.log("No hook to remove.");
|
|
8792
9020
|
}
|
|
8793
|
-
|
|
9021
|
+
throw err;
|
|
8794
9022
|
}
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
9023
|
+
try {
|
|
9024
|
+
resetHard(upstream);
|
|
9025
|
+
} catch (err) {
|
|
9026
|
+
try {
|
|
9027
|
+
deleteBranch(opts.newBranch);
|
|
9028
|
+
} catch {
|
|
8800
9029
|
}
|
|
8801
|
-
|
|
8802
|
-
|
|
9030
|
+
if (stashed) {
|
|
9031
|
+
try {
|
|
9032
|
+
stashPop();
|
|
9033
|
+
} catch {
|
|
9034
|
+
}
|
|
9035
|
+
}
|
|
9036
|
+
throw new Error(
|
|
9037
|
+
`Rescue aborted: failed to reset ${opts.currentBranch} to upstream. Your repo state has been restored. Original error: ${err?.message ?? String(err)}`
|
|
8803
9038
|
);
|
|
8804
|
-
process.exit(1);
|
|
8805
9039
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
9040
|
+
try {
|
|
9041
|
+
checkoutBranch(opts.newBranch);
|
|
9042
|
+
} catch (err) {
|
|
9043
|
+
try {
|
|
9044
|
+
resetHard(headSha);
|
|
9045
|
+
} catch {
|
|
9046
|
+
}
|
|
9047
|
+
if (stashed) {
|
|
9048
|
+
try {
|
|
9049
|
+
stashPop();
|
|
9050
|
+
} catch {
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
throw err;
|
|
9054
|
+
}
|
|
9055
|
+
if (stashed) {
|
|
9056
|
+
try {
|
|
9057
|
+
stashPop();
|
|
9058
|
+
} catch (err) {
|
|
9059
|
+
throw new Error(
|
|
9060
|
+
`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.
|
|
9061
|
+
Original error: ${err?.message ?? err}`
|
|
9062
|
+
);
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
return {
|
|
9066
|
+
newBranch: opts.newBranch,
|
|
9067
|
+
stashed,
|
|
9068
|
+
upstreamRef: upstream,
|
|
9069
|
+
movedFromSha: headSha
|
|
9070
|
+
};
|
|
8810
9071
|
}
|
|
8811
|
-
var
|
|
8812
|
-
|
|
8813
|
-
"src/commands/init.ts"() {
|
|
9072
|
+
var init_branch_rescue = __esm({
|
|
9073
|
+
"src/branch-rescue.ts"() {
|
|
8814
9074
|
"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
|
-
`;
|
|
9075
|
+
init_git();
|
|
8846
9076
|
}
|
|
8847
9077
|
});
|
|
8848
9078
|
|
|
8849
|
-
// src/
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
9079
|
+
// src/branch-detect.ts
|
|
9080
|
+
function matchGlob(name, pattern) {
|
|
9081
|
+
const n = name.toLowerCase();
|
|
9082
|
+
const p = pattern.toLowerCase();
|
|
9083
|
+
if (!p.includes("*")) return n === p;
|
|
9084
|
+
const escaped = p.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
9085
|
+
const withDoubleStar = escaped.replace(/\*\*/g, "\0DOUBLE_STAR\0");
|
|
9086
|
+
const withSingleStar = withDoubleStar.replace(/\*/g, "[^/]*");
|
|
9087
|
+
const final = withSingleStar.replace(/\x00DOUBLE_STAR\x00/g, ".*");
|
|
9088
|
+
const rx = new RegExp("^" + final + "$");
|
|
9089
|
+
return rx.test(n);
|
|
9090
|
+
}
|
|
9091
|
+
function isProtectedBranch(branch, protectedList) {
|
|
9092
|
+
if (!protectedList || protectedList.length === 0) return false;
|
|
9093
|
+
return protectedList.some((p) => matchGlob(branch, p));
|
|
9094
|
+
}
|
|
9095
|
+
function resolveProtectedBranches(opts) {
|
|
9096
|
+
const set = /* @__PURE__ */ new Set();
|
|
9097
|
+
if (opts.configList && opts.configList.length > 0) {
|
|
9098
|
+
for (const b of opts.configList) set.add(b);
|
|
9099
|
+
} else {
|
|
9100
|
+
for (const b of HARDCODED_PROTECTED) set.add(b);
|
|
9101
|
+
}
|
|
9102
|
+
if (opts.detectDefault && opts.defaultBranch) {
|
|
9103
|
+
set.add(opts.defaultBranch);
|
|
9104
|
+
}
|
|
9105
|
+
return Array.from(set);
|
|
9106
|
+
}
|
|
9107
|
+
var HARDCODED_PROTECTED;
|
|
9108
|
+
var init_branch_detect = __esm({
|
|
9109
|
+
"src/branch-detect.ts"() {
|
|
9110
|
+
"use strict";
|
|
9111
|
+
HARDCODED_PROTECTED = ["main", "master", "develop", "trunk"];
|
|
9112
|
+
}
|
|
8853
9113
|
});
|
|
8854
|
-
|
|
8855
|
-
|
|
9114
|
+
|
|
9115
|
+
// src/protected-branch-guard.ts
|
|
9116
|
+
function shouldRunGuard(opts) {
|
|
9117
|
+
if (opts.allowProtected) return false;
|
|
9118
|
+
if (opts.hookMode) return false;
|
|
9119
|
+
if (!opts.isTTY) return false;
|
|
9120
|
+
return true;
|
|
8856
9121
|
}
|
|
8857
|
-
function
|
|
8858
|
-
|
|
8859
|
-
const
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
9122
|
+
function detectProtectedBranchState(opts) {
|
|
9123
|
+
const branch = getCurrentBranch();
|
|
9124
|
+
const protectedList = resolveProtectedBranches({
|
|
9125
|
+
configList: opts.protectedBranches,
|
|
9126
|
+
detectDefault: opts.detectDefault !== false,
|
|
9127
|
+
defaultBranch: getDefaultBranch()
|
|
9128
|
+
});
|
|
9129
|
+
const protectedBranch = isProtectedBranch(branch, protectedList);
|
|
9130
|
+
if (!protectedBranch) {
|
|
9131
|
+
return { isProtected: false, branch, commitsAhead: 0, mode: "none" };
|
|
8866
9132
|
}
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
9133
|
+
const commitsAhead = getCommitsAheadOfUpstream(branch);
|
|
9134
|
+
const mode = commitsAhead > 0 ? "rescue" : "uncommitted";
|
|
9135
|
+
return { isProtected: true, branch, commitsAhead, mode };
|
|
9136
|
+
}
|
|
9137
|
+
var init_protected_branch_guard = __esm({
|
|
9138
|
+
"src/protected-branch-guard.ts"() {
|
|
9139
|
+
"use strict";
|
|
9140
|
+
init_branch_detect();
|
|
9141
|
+
init_git();
|
|
8870
9142
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
9143
|
+
});
|
|
9144
|
+
|
|
9145
|
+
// src/branch-name.ts
|
|
9146
|
+
function sanitizeBranchName(input) {
|
|
9147
|
+
if (!input) return null;
|
|
9148
|
+
let s = input.toLowerCase().trim();
|
|
9149
|
+
s = s.replace(/[\s_]+/g, "-");
|
|
9150
|
+
s = s.replace(/[^a-z0-9/-]/g, "");
|
|
9151
|
+
s = s.replace(/-+/g, "-").replace(/\/+/g, "/");
|
|
9152
|
+
s = s.replace(/^[-/]+|[-/]+$/g, "");
|
|
9153
|
+
if (!s.includes("/")) return null;
|
|
9154
|
+
if (s.length > MAX_BRANCH_NAME_LENGTH) {
|
|
9155
|
+
const parts = s.split("/");
|
|
9156
|
+
const type = parts[0] ?? "";
|
|
9157
|
+
const slugBudget = Math.min(MAX_BRANCH_NAME_LENGTH - type.length - 1, 52);
|
|
9158
|
+
if (slugBudget < 2) return null;
|
|
9159
|
+
s = `${type}/${parts.slice(1).join("/").slice(0, slugBudget).replace(/-+$/g, "")}`;
|
|
9160
|
+
}
|
|
9161
|
+
return validateBranchName(s) ? s : null;
|
|
9162
|
+
}
|
|
9163
|
+
function ensureUniqueName(name, exists) {
|
|
9164
|
+
if (!exists(name)) return name;
|
|
9165
|
+
for (let i = 2; i <= 100; i++) {
|
|
9166
|
+
const candidate = `${name}-${i}`;
|
|
9167
|
+
if (!exists(candidate)) return candidate;
|
|
8874
9168
|
}
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
9169
|
+
throw new Error(`Could not find a unique name for ${name} after 100 attempts`);
|
|
9170
|
+
}
|
|
9171
|
+
function finalizeBranchName(raw, exists, options = {}) {
|
|
9172
|
+
let candidate = raw;
|
|
9173
|
+
if (!validateBranchName(candidate)) {
|
|
9174
|
+
const s = sanitizeBranchName(candidate);
|
|
9175
|
+
if (!s) {
|
|
9176
|
+
throw new Error(`Generated invalid branch name and could not sanitize: ${raw}`);
|
|
9177
|
+
}
|
|
9178
|
+
candidate = s;
|
|
8878
9179
|
}
|
|
8879
|
-
|
|
9180
|
+
if (options.skipUniqueness) {
|
|
9181
|
+
return candidate;
|
|
9182
|
+
}
|
|
9183
|
+
return ensureUniqueName(candidate, exists);
|
|
8880
9184
|
}
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
".commitlintrc",
|
|
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
|
-
}
|
|
9185
|
+
var init_branch_name = __esm({
|
|
9186
|
+
"src/branch-name.ts"() {
|
|
9187
|
+
"use strict";
|
|
9188
|
+
init_dist();
|
|
8905
9189
|
}
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
9190
|
+
});
|
|
9191
|
+
|
|
9192
|
+
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
9193
|
+
var require_picocolors = __commonJS({
|
|
9194
|
+
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports2, module2) {
|
|
9195
|
+
var p = process || {};
|
|
9196
|
+
var argv = p.argv || [];
|
|
9197
|
+
var env = p.env || {};
|
|
9198
|
+
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);
|
|
9199
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
9200
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
9201
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
9202
|
+
};
|
|
9203
|
+
var replaceClose = (string, close, replace, index) => {
|
|
9204
|
+
let result = "", cursor = 0;
|
|
9205
|
+
do {
|
|
9206
|
+
result += string.substring(cursor, index) + replace;
|
|
9207
|
+
cursor = index + close.length;
|
|
9208
|
+
index = string.indexOf(close, cursor);
|
|
9209
|
+
} while (~index);
|
|
9210
|
+
return result + string.substring(cursor);
|
|
9211
|
+
};
|
|
9212
|
+
var createColors = (enabled = isColorSupported) => {
|
|
9213
|
+
let f = enabled ? formatter : () => String;
|
|
9214
|
+
return {
|
|
9215
|
+
isColorSupported: enabled,
|
|
9216
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
9217
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
9218
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
9219
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
9220
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
9221
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
9222
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
9223
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
9224
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
9225
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
9226
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
9227
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
9228
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
9229
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
9230
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
9231
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
9232
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
9233
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
9234
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
9235
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
9236
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
9237
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
9238
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
9239
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
9240
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
9241
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
9242
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
9243
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
9244
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
9245
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
9246
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
9247
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
9248
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
9249
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
9250
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
9251
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
9252
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
9253
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
9254
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
9255
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
9256
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
9257
|
+
};
|
|
9258
|
+
};
|
|
9259
|
+
module2.exports = createColors();
|
|
9260
|
+
module2.exports.createColors = createColors;
|
|
8917
9261
|
}
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
9262
|
+
});
|
|
9263
|
+
|
|
9264
|
+
// src/ui.ts
|
|
9265
|
+
function hasCliNoColor() {
|
|
9266
|
+
try {
|
|
9267
|
+
return process.argv.slice(2).includes("--no-color");
|
|
9268
|
+
} catch {
|
|
9269
|
+
return false;
|
|
8921
9270
|
}
|
|
8922
|
-
return null;
|
|
8923
9271
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
9272
|
+
function getTerminalWidth() {
|
|
9273
|
+
return process.stderr.columns ?? process.stdout.columns ?? 80;
|
|
9274
|
+
}
|
|
9275
|
+
function createUI(options) {
|
|
9276
|
+
const isColor = options.isTTY && !options.noColor;
|
|
9277
|
+
const wrap = (fn) => (s) => isColor ? fn(s) : s;
|
|
9278
|
+
const format = {
|
|
9279
|
+
step: (msg) => `${isColor ? import_picocolors.default.dim("\u203A") : "\u203A"} ${isColor ? import_picocolors.default.dim(msg) : msg}`,
|
|
9280
|
+
success: (msg) => `${isColor ? import_picocolors.default.green("\u2713") : "\u2713"} ${msg}`,
|
|
9281
|
+
error: (msg) => `${isColor ? import_picocolors.default.red("\u2717") : "\u2717"} ${msg}`,
|
|
9282
|
+
dim: wrap(import_picocolors.default.dim),
|
|
9283
|
+
bold: wrap(import_picocolors.default.bold),
|
|
9284
|
+
commitType: wrap(import_picocolors.default.cyan),
|
|
9285
|
+
commitScope: wrap(import_picocolors.default.yellow),
|
|
9286
|
+
accent: wrap(import_picocolors.default.magenta)
|
|
9287
|
+
};
|
|
9288
|
+
function createSpinner(message, write = (s) => process.stderr.write(s)) {
|
|
9289
|
+
let frame = 0;
|
|
9290
|
+
let interval = null;
|
|
9291
|
+
return {
|
|
9292
|
+
start() {
|
|
9293
|
+
if (interval) return;
|
|
9294
|
+
if (!options.isTTY) return;
|
|
9295
|
+
interval = setInterval(() => {
|
|
9296
|
+
const f = SPINNER_FRAMES2[frame++ % SPINNER_FRAMES2.length];
|
|
9297
|
+
write(`\r${format.step(message)} ${isColor ? import_picocolors.default.cyan(f) : f}`);
|
|
9298
|
+
}, 80);
|
|
9299
|
+
},
|
|
9300
|
+
stop(finalMessage) {
|
|
9301
|
+
if (interval) {
|
|
9302
|
+
clearInterval(interval);
|
|
9303
|
+
interval = null;
|
|
9304
|
+
}
|
|
9305
|
+
if (options.isTTY) {
|
|
9306
|
+
write("\r\x1B[2K");
|
|
9307
|
+
}
|
|
9308
|
+
if (finalMessage) {
|
|
9309
|
+
write(finalMessage + "\n");
|
|
8946
9310
|
}
|
|
8947
|
-
await api.pushTeamRules(rules);
|
|
8948
|
-
console.log("Team rules updated from local commitlint config.");
|
|
8949
|
-
} else {
|
|
8950
|
-
const rules = await api.getTeamRules();
|
|
8951
|
-
console.log("\n Team Commit Rules:");
|
|
8952
|
-
console.log(JSON.stringify(rules, null, 2));
|
|
8953
|
-
}
|
|
8954
|
-
break;
|
|
8955
|
-
}
|
|
8956
|
-
case "invite": {
|
|
8957
|
-
const email = args?.[0];
|
|
8958
|
-
if (!email) {
|
|
8959
|
-
console.error("Usage: qc team invite <email>");
|
|
8960
|
-
process.exit(1);
|
|
8961
9311
|
}
|
|
8962
|
-
|
|
8963
|
-
console.log(`Invitation sent to ${email}`);
|
|
8964
|
-
break;
|
|
8965
|
-
}
|
|
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);
|
|
9312
|
+
};
|
|
8970
9313
|
}
|
|
9314
|
+
const log = {
|
|
9315
|
+
step: (msg) => process.stderr.write(format.step(msg) + "\n"),
|
|
9316
|
+
success: (msg) => process.stderr.write(format.success(msg) + "\n"),
|
|
9317
|
+
error: (msg) => process.stderr.write(format.error(msg) + "\n"),
|
|
9318
|
+
dim: (msg) => process.stderr.write(format.dim(msg) + "\n")
|
|
9319
|
+
};
|
|
9320
|
+
return { isColor, format, spinner: createSpinner, log };
|
|
8971
9321
|
}
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
9322
|
+
function getUI() {
|
|
9323
|
+
if (!_defaultUI) {
|
|
9324
|
+
_defaultUI = createUI({
|
|
9325
|
+
isTTY: !!process.stderr.isTTY,
|
|
9326
|
+
noColor: !!process.env.NO_COLOR || hasCliNoColor()
|
|
9327
|
+
});
|
|
9328
|
+
}
|
|
9329
|
+
return _defaultUI;
|
|
9330
|
+
}
|
|
9331
|
+
var import_picocolors, SPINNER_FRAMES2, _defaultUI, ui;
|
|
9332
|
+
var init_ui = __esm({
|
|
9333
|
+
"src/ui.ts"() {
|
|
8975
9334
|
"use strict";
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
9335
|
+
import_picocolors = __toESM(require_picocolors());
|
|
9336
|
+
SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
9337
|
+
ui = new Proxy({}, {
|
|
9338
|
+
get(_target, prop) {
|
|
9339
|
+
return getUI()[prop];
|
|
9340
|
+
}
|
|
9341
|
+
});
|
|
8980
9342
|
}
|
|
8981
9343
|
});
|
|
8982
9344
|
|
|
8983
|
-
// src/
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
}
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
9345
|
+
// src/ui-rich.ts
|
|
9346
|
+
function splitCommitForBox(message) {
|
|
9347
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
9348
|
+
const m = firstLine.match(HEADER_RX);
|
|
9349
|
+
if (!m) return { type: null, scope: null, subject: firstLine, breaking: false };
|
|
9350
|
+
return {
|
|
9351
|
+
type: m[1] ?? null,
|
|
9352
|
+
scope: m[2] ?? null,
|
|
9353
|
+
breaking: m[3] === "!",
|
|
9354
|
+
subject: m[4] ?? ""
|
|
9355
|
+
};
|
|
9356
|
+
}
|
|
9357
|
+
function renderFileTree(files, maxFiles) {
|
|
9358
|
+
if (files.length === 0) return "";
|
|
9359
|
+
const lines = [];
|
|
9360
|
+
const display = files.slice(0, maxFiles);
|
|
9361
|
+
const overflow = Math.max(0, files.length - maxFiles);
|
|
9362
|
+
for (let i = 0; i < display.length; i++) {
|
|
9363
|
+
const isLast = i === display.length - 1 && overflow === 0;
|
|
9364
|
+
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
9365
|
+
lines.push(` ${connector} ${display[i]}`);
|
|
9366
|
+
}
|
|
9367
|
+
if (overflow > 0) {
|
|
9368
|
+
lines.push(` \u2514\u2500 +${overflow} more files`);
|
|
8992
9369
|
}
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9370
|
+
return lines.join("\n");
|
|
9371
|
+
}
|
|
9372
|
+
function wrapLine(text, width) {
|
|
9373
|
+
if (text.length <= width) return [text];
|
|
9374
|
+
const result = [];
|
|
9375
|
+
let remaining = text;
|
|
9376
|
+
while (remaining.length > width) {
|
|
9377
|
+
let breakAt = remaining.lastIndexOf(" ", width);
|
|
9378
|
+
if (breakAt < width / 2) breakAt = width;
|
|
9379
|
+
result.push(remaining.slice(0, breakAt));
|
|
9380
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
9381
|
+
}
|
|
9382
|
+
if (remaining) result.push(remaining);
|
|
9383
|
+
return result;
|
|
9384
|
+
}
|
|
9385
|
+
function stripAnsi(s) {
|
|
9386
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
9387
|
+
}
|
|
9388
|
+
function boxedLine(content, innerWidth, isColor) {
|
|
9389
|
+
const visibleLen = stripAnsi(content).length;
|
|
9390
|
+
const padding = " ".repeat(Math.max(0, innerWidth - visibleLen));
|
|
9391
|
+
const border = isColor ? import_picocolors2.default.dim("\u2502") : "\u2502";
|
|
9392
|
+
return ` ${border} ${content}${padding} ${border}`;
|
|
9393
|
+
}
|
|
9394
|
+
function renderBoxedCommit(header, body, opts) {
|
|
9395
|
+
if (opts.width < MIN_BOX_WIDTH) {
|
|
9396
|
+
const lines2 = [header.split("\n")[0] ?? header];
|
|
9397
|
+
if (body) lines2.push("", body);
|
|
9398
|
+
return lines2.join("\n");
|
|
9399
|
+
}
|
|
9400
|
+
const innerWidth = opts.width - 6;
|
|
9401
|
+
const horiz = opts.width - 2;
|
|
9402
|
+
const top = " " + (opts.isColor ? import_picocolors2.default.dim("\u256D" + "\u2500".repeat(horiz) + "\u256E") : "\u256D" + "\u2500".repeat(horiz) + "\u256E");
|
|
9403
|
+
const bottom = " " + (opts.isColor ? import_picocolors2.default.dim("\u2570" + "\u2500".repeat(horiz) + "\u256F") : "\u2570" + "\u2500".repeat(horiz) + "\u256F");
|
|
9404
|
+
const parsed = splitCommitForBox(header);
|
|
9405
|
+
let firstLineStyled;
|
|
9406
|
+
if (parsed.type && parsed.scope) {
|
|
9407
|
+
const bare = `${parsed.type}(${parsed.scope})${parsed.breaking ? "!" : ""}: ${parsed.subject}`;
|
|
9408
|
+
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;
|
|
9409
|
+
} else if (parsed.type) {
|
|
9410
|
+
const bare = `${parsed.type}${parsed.breaking ? "!" : ""}: ${parsed.subject}`;
|
|
9411
|
+
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;
|
|
9412
|
+
} else {
|
|
9413
|
+
firstLineStyled = header.split("\n")[0] ?? header;
|
|
9004
9414
|
}
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9415
|
+
const lines = [];
|
|
9416
|
+
const headerParts = wrapLine(firstLineStyled, innerWidth);
|
|
9417
|
+
for (let i = 0; i < headerParts.length; i++) {
|
|
9418
|
+
const indent = i === 0 ? "" : " ";
|
|
9419
|
+
lines.push(boxedLine(indent + (headerParts[i] ?? ""), innerWidth, opts.isColor));
|
|
9008
9420
|
}
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9421
|
+
if (body) {
|
|
9422
|
+
lines.push(boxedLine("", innerWidth, opts.isColor));
|
|
9423
|
+
for (const bline of body.split("\n")) {
|
|
9424
|
+
const trimmed = bline.trim();
|
|
9425
|
+
if (!trimmed) continue;
|
|
9426
|
+
const rendered = trimmed.replace(/^[-*]\s+/, opts.isColor ? `${import_picocolors2.default.green("\u2022")} ` : "\u2022 ");
|
|
9427
|
+
const wrapped = wrapLine(rendered, innerWidth);
|
|
9428
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
9429
|
+
lines.push(boxedLine((i === 0 ? "" : " ") + (wrapped[i] ?? ""), innerWidth, opts.isColor));
|
|
9430
|
+
}
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9433
|
+
return [top, ...lines, bottom].join("\n");
|
|
9012
9434
|
}
|
|
9013
|
-
function
|
|
9014
|
-
const
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
console.log(` auto_stage: ${cfg.autoStage ? "true" : "false"}`);
|
|
9021
|
-
console.log(` auth: ${apiKey ? "****" : "not set"}`);
|
|
9022
|
-
if (cfg.excludes?.length) {
|
|
9023
|
-
console.log(` excludes: ${cfg.excludes.join(", ")}`);
|
|
9024
|
-
}
|
|
9025
|
-
}
|
|
9026
|
-
function setConfig(key, value) {
|
|
9027
|
-
const cfg = getConfig();
|
|
9028
|
-
const updates = {};
|
|
9029
|
-
if (key === "model") {
|
|
9030
|
-
updates.model = value;
|
|
9031
|
-
} else if (key === "provider") {
|
|
9032
|
-
const valid = ["ollama", "lmstudio", "openrouter", "custom", "cloudflare"];
|
|
9033
|
-
if (!valid.includes(value.toLowerCase())) {
|
|
9034
|
-
console.error(`Invalid provider. Must be one of: ${valid.join(", ")}`);
|
|
9035
|
-
process.exit(1);
|
|
9036
|
-
}
|
|
9037
|
-
updates.provider = value.toLowerCase();
|
|
9038
|
-
} else if (key === "api_url") {
|
|
9039
|
-
try {
|
|
9040
|
-
new URL(value);
|
|
9041
|
-
updates.apiUrl = value;
|
|
9042
|
-
} catch {
|
|
9043
|
-
console.error("Invalid URL:", value);
|
|
9044
|
-
process.exit(1);
|
|
9045
|
-
}
|
|
9046
|
-
} else if (key === "auto_stage") {
|
|
9047
|
-
updates.autoStage = value === "true" || value === "1";
|
|
9048
|
-
} else {
|
|
9049
|
-
console.error(`Unknown key: ${key}`);
|
|
9050
|
-
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
9051
|
-
process.exit(1);
|
|
9052
|
-
}
|
|
9053
|
-
saveConfig({ ...cfg, ...updates });
|
|
9054
|
-
console.log(`Set ${key} = ${value}`);
|
|
9435
|
+
function renderStatsLine(stats, isColor) {
|
|
9436
|
+
const parts = [];
|
|
9437
|
+
parts.push(`${stats.files} files`);
|
|
9438
|
+
parts.push(`+${stats.additions} \u2212${stats.deletions}`);
|
|
9439
|
+
if (stats.tokens !== void 0) parts.push(`${stats.tokens} tokens`);
|
|
9440
|
+
const text = parts.join(" \xB7 ");
|
|
9441
|
+
return isColor ? ` ${import_picocolors2.default.dim(text)}` : ` ${text}`;
|
|
9055
9442
|
}
|
|
9056
|
-
function
|
|
9057
|
-
|
|
9058
|
-
|
|
9443
|
+
function shouldUseRichOutput(opts) {
|
|
9444
|
+
if (!opts.isTTY) return false;
|
|
9445
|
+
if (opts.noColor) return false;
|
|
9446
|
+
if (opts.style !== "rich") return false;
|
|
9447
|
+
if (opts.width < MIN_BOX_WIDTH) return false;
|
|
9448
|
+
return true;
|
|
9059
9449
|
}
|
|
9060
|
-
var
|
|
9061
|
-
|
|
9450
|
+
var import_picocolors2, HEADER_RX, MIN_BOX_WIDTH;
|
|
9451
|
+
var init_ui_rich = __esm({
|
|
9452
|
+
"src/ui-rich.ts"() {
|
|
9062
9453
|
"use strict";
|
|
9063
|
-
|
|
9064
|
-
|
|
9454
|
+
import_picocolors2 = __toESM(require_picocolors());
|
|
9455
|
+
init_ui();
|
|
9456
|
+
HEADER_RX = /^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*(.*)$/;
|
|
9457
|
+
MIN_BOX_WIDTH = 60;
|
|
9065
9458
|
}
|
|
9066
9459
|
});
|
|
9067
9460
|
|
|
9068
|
-
// src/
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
}
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9461
|
+
// src/commit-helpers.ts
|
|
9462
|
+
function applyCliTypeScopeToRules(rules, type, scope) {
|
|
9463
|
+
let next = { ...rules };
|
|
9464
|
+
if (type) {
|
|
9465
|
+
next = { ...next, types: [type] };
|
|
9466
|
+
}
|
|
9467
|
+
if (scope) {
|
|
9468
|
+
next = { ...next, scopes: [scope] };
|
|
9469
|
+
}
|
|
9470
|
+
return next;
|
|
9471
|
+
}
|
|
9472
|
+
function generationHintsFromArgs(split, forceBody) {
|
|
9473
|
+
const h = {};
|
|
9474
|
+
if (split) h.split = true;
|
|
9475
|
+
if (forceBody) h.force_body = true;
|
|
9476
|
+
return Object.keys(h).length > 0 ? h : void 0;
|
|
9477
|
+
}
|
|
9478
|
+
function splitCommitMessageForDisplay(message) {
|
|
9479
|
+
const t = message.replace(/\r\n/g, "\n").trimEnd();
|
|
9480
|
+
const doubleNl = t.indexOf("\n\n");
|
|
9481
|
+
if (doubleNl !== -1) {
|
|
9482
|
+
const head = t.slice(0, doubleNl);
|
|
9483
|
+
const subject = head.split("\n")[0]?.trim() ?? "";
|
|
9484
|
+
return { subject, body: t.slice(doubleNl + 2).trimEnd() };
|
|
9485
|
+
}
|
|
9486
|
+
const firstNl = t.indexOf("\n");
|
|
9487
|
+
if (firstNl === -1) {
|
|
9488
|
+
return { subject: t.trim(), body: "" };
|
|
9489
|
+
}
|
|
9490
|
+
return {
|
|
9491
|
+
subject: t.slice(0, firstNl).trim(),
|
|
9492
|
+
body: t.slice(firstNl + 1).trimEnd()
|
|
9493
|
+
};
|
|
9494
|
+
}
|
|
9495
|
+
function formatVerboseCommitDiagnostics(diagnostics, roundTripMs) {
|
|
9496
|
+
const lines = [`api_round_trip_ms: ${roundTripMs}`];
|
|
9497
|
+
if (diagnostics !== void 0) {
|
|
9498
|
+
lines.push(JSON.stringify(diagnostics, null, 2));
|
|
9499
|
+
}
|
|
9500
|
+
return lines.join("\n");
|
|
9501
|
+
}
|
|
9502
|
+
async function interactiveRefineMessage(initial, opts) {
|
|
9503
|
+
if (opts.skip) return { action: "accept", message: initial };
|
|
9504
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9505
|
+
try {
|
|
9506
|
+
process.stderr.write(`
|
|
9507
|
+
${initial}
|
|
9508
|
+
|
|
9076
9509
|
`);
|
|
9510
|
+
const choice = (await rl.question("Keep? [Y/n/e]: ")).trim().toLowerCase();
|
|
9511
|
+
if (choice === "n") {
|
|
9512
|
+
return { action: "abort" };
|
|
9513
|
+
}
|
|
9514
|
+
if (choice === "e") {
|
|
9515
|
+
process.stderr.write("Enter new message (end with a line containing only .):\n");
|
|
9516
|
+
const lines = [];
|
|
9517
|
+
while (true) {
|
|
9518
|
+
const line = await rl.question("");
|
|
9519
|
+
if (line === ".") break;
|
|
9520
|
+
lines.push(line);
|
|
9521
|
+
}
|
|
9522
|
+
const edited = lines.join("\n").trim();
|
|
9523
|
+
return { action: "edit", message: edited.length > 0 ? edited : initial };
|
|
9524
|
+
}
|
|
9525
|
+
return { action: "accept", message: initial };
|
|
9526
|
+
} finally {
|
|
9527
|
+
rl.close();
|
|
9528
|
+
}
|
|
9529
|
+
}
|
|
9530
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
9531
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9532
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
9077
9533
|
try {
|
|
9078
|
-
const
|
|
9079
|
-
if (
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9534
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
9535
|
+
if (answer === "n" || answer === "no") return false;
|
|
9536
|
+
if (answer === "y" || answer === "yes") return true;
|
|
9537
|
+
return defaultYes;
|
|
9538
|
+
} finally {
|
|
9539
|
+
rl.close();
|
|
9540
|
+
}
|
|
9541
|
+
}
|
|
9542
|
+
async function confirmCommit(prompt2, opts) {
|
|
9543
|
+
if (opts.skip) return { action: "commit" };
|
|
9544
|
+
const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
9545
|
+
try {
|
|
9546
|
+
const ans = (await rl.question(prompt2)).trim().toLowerCase();
|
|
9547
|
+
if (ans !== "y" && ans !== "yes") {
|
|
9548
|
+
return { action: "abort" };
|
|
9085
9549
|
}
|
|
9086
|
-
|
|
9087
|
-
|
|
9550
|
+
return { action: "commit" };
|
|
9551
|
+
} finally {
|
|
9552
|
+
rl.close();
|
|
9088
9553
|
}
|
|
9089
9554
|
}
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9555
|
+
function shouldSkipTTYInteraction(hookMode) {
|
|
9556
|
+
return hookMode === true || process.stdin.isTTY !== true;
|
|
9557
|
+
}
|
|
9558
|
+
function logVerboseDiagnostics(dim, verbose, quiet, diagnostics, roundTripMs) {
|
|
9559
|
+
if (!verbose || quiet) return;
|
|
9560
|
+
process.stderr.write(
|
|
9561
|
+
`
|
|
9562
|
+
${formatVerboseCommitDiagnostics(diagnostics, roundTripMs)}
|
|
9563
|
+
`
|
|
9564
|
+
);
|
|
9565
|
+
dim("(verbose diagnostics on stderr)");
|
|
9566
|
+
}
|
|
9567
|
+
function isDisplayOpts(opts) {
|
|
9568
|
+
return typeof opts === "object" && opts !== null && "log" in opts;
|
|
9569
|
+
}
|
|
9570
|
+
function createSilentLog() {
|
|
9571
|
+
return {
|
|
9572
|
+
step: () => {
|
|
9573
|
+
},
|
|
9574
|
+
success: () => {
|
|
9575
|
+
},
|
|
9576
|
+
error: (msg) => console.error(msg),
|
|
9577
|
+
dim: () => {
|
|
9578
|
+
}
|
|
9579
|
+
};
|
|
9580
|
+
}
|
|
9581
|
+
function displayCommitMessage(message, opts) {
|
|
9582
|
+
const display = isDisplayOpts(opts) ? opts : { log: opts };
|
|
9583
|
+
const log = display.log;
|
|
9584
|
+
const { subject, body } = splitCommitMessageForDisplay(message);
|
|
9585
|
+
const tw = getTerminalWidth();
|
|
9586
|
+
const useRich = shouldUseRichOutput({
|
|
9587
|
+
isTTY: display.isTTY ?? !!process.stderr.isTTY,
|
|
9588
|
+
noColor: display.isColor === false,
|
|
9589
|
+
width: tw,
|
|
9590
|
+
style: display.style ?? "rich"
|
|
9591
|
+
});
|
|
9592
|
+
if (useRich) {
|
|
9593
|
+
const tree = display.stagedFiles && display.stagedFiles.length > 0 ? renderFileTree(display.stagedFiles, 8) : "";
|
|
9594
|
+
if (tree) {
|
|
9595
|
+
process.stderr.write(tree + "\n");
|
|
9596
|
+
}
|
|
9597
|
+
const boxed = renderBoxedCommit(subject, body, {
|
|
9598
|
+
width: Math.min(Math.max(tw - 4, 60), 80),
|
|
9599
|
+
isColor: !!display.isColor
|
|
9600
|
+
});
|
|
9601
|
+
process.stderr.write(boxed + "\n");
|
|
9602
|
+
if (display.stats) {
|
|
9603
|
+
process.stderr.write(renderStatsLine(display.stats, !!display.isColor) + "\n");
|
|
9604
|
+
}
|
|
9605
|
+
return;
|
|
9606
|
+
}
|
|
9607
|
+
log.success(subject);
|
|
9608
|
+
if (body) {
|
|
9609
|
+
for (const line of body.split("\n")) {
|
|
9610
|
+
log.dim(` ${line}`);
|
|
9611
|
+
}
|
|
9612
|
+
process.stderr.write("\n");
|
|
9613
|
+
}
|
|
9614
|
+
}
|
|
9615
|
+
var import_promises;
|
|
9616
|
+
var init_commit_helpers = __esm({
|
|
9617
|
+
"src/commit-helpers.ts"() {
|
|
9093
9618
|
"use strict";
|
|
9094
|
-
|
|
9619
|
+
import_promises = __toESM(require("node:readline/promises"));
|
|
9620
|
+
init_ui_rich();
|
|
9095
9621
|
}
|
|
9096
9622
|
});
|
|
9097
9623
|
|
|
@@ -9133,10 +9659,17 @@ function isMinified(content) {
|
|
|
9133
9659
|
if (lines.length === 0) return false;
|
|
9134
9660
|
return lines.some((l) => l.length > 500);
|
|
9135
9661
|
}
|
|
9136
|
-
function
|
|
9662
|
+
function buildFileSummary(file) {
|
|
9663
|
+
const sizeKB = Math.round(file.content.length / 1024);
|
|
9664
|
+
return `[modified: ${sanitizeFilepath(file.filepath)} \u2014 +${file.additions} \u2212${file.deletions} lines, ~${sizeKB}KB]
|
|
9665
|
+
`;
|
|
9666
|
+
}
|
|
9667
|
+
function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
|
|
9137
9668
|
const files = parseDiffIntoFiles(diff);
|
|
9138
|
-
if (files.length === 0)
|
|
9139
|
-
|
|
9669
|
+
if (files.length === 0) {
|
|
9670
|
+
return { processedDiff: diff, summarized: [], aggressivelySummarized: [], tokensSaved: 0 };
|
|
9671
|
+
}
|
|
9672
|
+
const entries = [];
|
|
9140
9673
|
const summarized = [];
|
|
9141
9674
|
let tokensSaved = 0;
|
|
9142
9675
|
for (const file of files) {
|
|
@@ -9145,41 +9678,121 @@ function preprocessDiff(diff) {
|
|
|
9145
9678
|
case "sourcemap":
|
|
9146
9679
|
tokensSaved += estimateTokens(file.content);
|
|
9147
9680
|
summarized.push(file.filepath);
|
|
9681
|
+
entries.push({ file, isNoise: true, summaryLine: null });
|
|
9148
9682
|
break;
|
|
9149
9683
|
case "lock":
|
|
9150
9684
|
tokensSaved += estimateTokens(file.content);
|
|
9151
|
-
kept.push(`[lock file updated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions} lines)]
|
|
9152
|
-
`);
|
|
9153
9685
|
summarized.push(file.filepath);
|
|
9686
|
+
entries.push({
|
|
9687
|
+
file,
|
|
9688
|
+
isNoise: true,
|
|
9689
|
+
summaryLine: `[lock file updated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions} lines)]
|
|
9690
|
+
`
|
|
9691
|
+
});
|
|
9154
9692
|
break;
|
|
9155
9693
|
case "generated":
|
|
9156
9694
|
tokensSaved += estimateTokens(file.content);
|
|
9157
|
-
kept.push(`[generated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions})]
|
|
9158
|
-
`);
|
|
9159
9695
|
summarized.push(file.filepath);
|
|
9696
|
+
entries.push({
|
|
9697
|
+
file,
|
|
9698
|
+
isNoise: true,
|
|
9699
|
+
summaryLine: `[generated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions})]
|
|
9700
|
+
`
|
|
9701
|
+
});
|
|
9160
9702
|
break;
|
|
9161
9703
|
case "vendored":
|
|
9162
9704
|
tokensSaved += estimateTokens(file.content);
|
|
9163
|
-
kept.push(`[vendored: ${sanitizeFilepath(file.filepath)} updated]
|
|
9164
|
-
`);
|
|
9165
9705
|
summarized.push(file.filepath);
|
|
9706
|
+
entries.push({
|
|
9707
|
+
file,
|
|
9708
|
+
isNoise: true,
|
|
9709
|
+
summaryLine: `[vendored: ${sanitizeFilepath(file.filepath)} updated]
|
|
9710
|
+
`
|
|
9711
|
+
});
|
|
9166
9712
|
break;
|
|
9167
9713
|
case "code":
|
|
9168
9714
|
if (isMinified(file.content)) {
|
|
9169
9715
|
tokensSaved += estimateTokens(file.content);
|
|
9170
9716
|
const sizeKB = Math.round(file.content.length / 1024);
|
|
9171
|
-
kept.push(`[minified asset: ${sanitizeFilepath(file.filepath)} (${sizeKB} KB)]
|
|
9172
|
-
`);
|
|
9173
9717
|
summarized.push(file.filepath);
|
|
9718
|
+
entries.push({
|
|
9719
|
+
file,
|
|
9720
|
+
isNoise: true,
|
|
9721
|
+
summaryLine: `[minified asset: ${sanitizeFilepath(file.filepath)} (${sizeKB} KB)]
|
|
9722
|
+
`
|
|
9723
|
+
});
|
|
9174
9724
|
} else {
|
|
9175
|
-
|
|
9725
|
+
entries.push({ file, isNoise: false, summaryLine: null });
|
|
9176
9726
|
}
|
|
9177
9727
|
break;
|
|
9178
9728
|
}
|
|
9179
9729
|
}
|
|
9730
|
+
const aggressiveMap = /* @__PURE__ */ new Map();
|
|
9731
|
+
function buildOutput() {
|
|
9732
|
+
const parts = [];
|
|
9733
|
+
for (const entry of entries) {
|
|
9734
|
+
if (entry.isNoise) {
|
|
9735
|
+
if (entry.summaryLine !== null) parts.push(entry.summaryLine);
|
|
9736
|
+
} else if (aggressiveMap.has(entry.file.filepath)) {
|
|
9737
|
+
parts.push(aggressiveMap.get(entry.file.filepath));
|
|
9738
|
+
} else {
|
|
9739
|
+
parts.push(entry.file.content);
|
|
9740
|
+
}
|
|
9741
|
+
}
|
|
9742
|
+
return parts.join("");
|
|
9743
|
+
}
|
|
9744
|
+
const codeEntries = entries.filter((e) => !e.isNoise);
|
|
9745
|
+
let output = buildOutput();
|
|
9746
|
+
if (output.length <= maxBytes) {
|
|
9747
|
+
return {
|
|
9748
|
+
processedDiff: output,
|
|
9749
|
+
summarized,
|
|
9750
|
+
aggressivelySummarized: [],
|
|
9751
|
+
tokensSaved
|
|
9752
|
+
};
|
|
9753
|
+
}
|
|
9754
|
+
const TIER1_THRESHOLD = 5 * 1024;
|
|
9755
|
+
for (const entry of codeEntries) {
|
|
9756
|
+
if (entry.file.content.length > TIER1_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
|
|
9757
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9758
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
output = buildOutput();
|
|
9762
|
+
if (output.length <= maxBytes) {
|
|
9763
|
+
return {
|
|
9764
|
+
processedDiff: output,
|
|
9765
|
+
summarized,
|
|
9766
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9767
|
+
tokensSaved
|
|
9768
|
+
};
|
|
9769
|
+
}
|
|
9770
|
+
const TIER2_THRESHOLD = 2 * 1024;
|
|
9771
|
+
for (const entry of codeEntries) {
|
|
9772
|
+
if (entry.file.content.length > TIER2_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
|
|
9773
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9774
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
9777
|
+
output = buildOutput();
|
|
9778
|
+
if (output.length <= maxBytes) {
|
|
9779
|
+
return {
|
|
9780
|
+
processedDiff: output,
|
|
9781
|
+
summarized,
|
|
9782
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9783
|
+
tokensSaved
|
|
9784
|
+
};
|
|
9785
|
+
}
|
|
9786
|
+
for (const entry of codeEntries) {
|
|
9787
|
+
if (!aggressiveMap.has(entry.file.filepath)) {
|
|
9788
|
+
tokensSaved += estimateTokens(entry.file.content);
|
|
9789
|
+
aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9180
9792
|
return {
|
|
9181
|
-
processedDiff:
|
|
9793
|
+
processedDiff: buildOutput(),
|
|
9182
9794
|
summarized,
|
|
9795
|
+
aggressivelySummarized: [...aggressiveMap.keys()],
|
|
9183
9796
|
tokensSaved
|
|
9184
9797
|
};
|
|
9185
9798
|
}
|
|
@@ -9211,612 +9824,1257 @@ var init_smart_diff = __esm({
|
|
|
9211
9824
|
}
|
|
9212
9825
|
});
|
|
9213
9826
|
|
|
9214
|
-
//
|
|
9215
|
-
var
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
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);
|
|
9233
|
-
};
|
|
9234
|
-
var createColors = (enabled = isColorSupported) => {
|
|
9235
|
-
let f = enabled ? formatter : () => String;
|
|
9236
|
-
return {
|
|
9237
|
-
isColorSupported: enabled,
|
|
9238
|
-
reset: f("\x1B[0m", "\x1B[0m"),
|
|
9239
|
-
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
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
|
-
};
|
|
9280
|
-
};
|
|
9281
|
-
module2.exports = createColors();
|
|
9282
|
-
module2.exports.createColors = createColors;
|
|
9283
|
-
}
|
|
9827
|
+
// src/local.ts
|
|
9828
|
+
var local_exports = {};
|
|
9829
|
+
__export(local_exports, {
|
|
9830
|
+
generateLocalBranchName: () => generateLocalBranchName,
|
|
9831
|
+
getLocalProviderConfig: () => getLocalProviderConfig,
|
|
9832
|
+
runLocalBranch: () => runLocalBranch,
|
|
9833
|
+
runLocalCommit: () => runLocalCommit
|
|
9284
9834
|
});
|
|
9285
|
-
|
|
9286
|
-
// src/ui.ts
|
|
9287
|
-
function hasCliNoColor() {
|
|
9835
|
+
function getLegacyProvider() {
|
|
9288
9836
|
try {
|
|
9289
|
-
|
|
9837
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "provider");
|
|
9838
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9839
|
+
const v = (0, import_fs8.readFileSync)(p, "utf-8").trim().toLowerCase();
|
|
9840
|
+
if (["ollama", "lmstudio", "openrouter", "custom", "cloudflare"].includes(v)) {
|
|
9841
|
+
return v;
|
|
9842
|
+
}
|
|
9843
|
+
}
|
|
9290
9844
|
} catch {
|
|
9291
|
-
return false;
|
|
9292
9845
|
}
|
|
9846
|
+
return null;
|
|
9293
9847
|
}
|
|
9294
|
-
function
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9848
|
+
function getLegacyBaseUrl(provider) {
|
|
9849
|
+
try {
|
|
9850
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "base_url");
|
|
9851
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9852
|
+
return (0, import_fs8.readFileSync)(p, "utf-8").trim();
|
|
9853
|
+
}
|
|
9854
|
+
} catch {
|
|
9855
|
+
}
|
|
9856
|
+
return PROVIDER_URLS[provider] ?? "";
|
|
9857
|
+
}
|
|
9858
|
+
function getLegacyModel(provider) {
|
|
9859
|
+
try {
|
|
9860
|
+
const p = (0, import_path8.join)(CONFIG_PATH2, "model");
|
|
9861
|
+
if ((0, import_fs8.existsSync)(p)) {
|
|
9862
|
+
const v = (0, import_fs8.readFileSync)(p, "utf-8").trim();
|
|
9863
|
+
if (v) return v;
|
|
9864
|
+
}
|
|
9865
|
+
} catch {
|
|
9866
|
+
}
|
|
9867
|
+
return DEFAULT_MODELS[provider] ?? "";
|
|
9868
|
+
}
|
|
9869
|
+
function getLocalProviderConfig() {
|
|
9870
|
+
const config2 = getConfig();
|
|
9871
|
+
const provider = config2.provider ?? getLegacyProvider();
|
|
9872
|
+
if (!provider) return null;
|
|
9873
|
+
const baseUrl = config2.apiUrl ?? getLegacyBaseUrl(provider) ?? PROVIDER_URLS[provider] ?? "";
|
|
9874
|
+
if (!baseUrl) return null;
|
|
9875
|
+
const model = config2.model ?? getLegacyModel(provider) ?? DEFAULT_MODELS[provider];
|
|
9876
|
+
const apiKey = provider === "openrouter" || provider === "custom" ? getApiKey() : null;
|
|
9877
|
+
if (provider === "openrouter" && !apiKey) return null;
|
|
9878
|
+
return { provider, baseUrl, model, apiKey };
|
|
9879
|
+
}
|
|
9880
|
+
function buildUserPrompt(changes, diff, rules, recentCommits, hints) {
|
|
9881
|
+
let prompt2 = `Generate a commit message for these changes:
|
|
9882
|
+
|
|
9883
|
+
## File changes:
|
|
9884
|
+
<file_changes>
|
|
9885
|
+
${changes}
|
|
9886
|
+
</file_changes>
|
|
9887
|
+
|
|
9888
|
+
## Diff:
|
|
9889
|
+
<diff>
|
|
9890
|
+
${diff}
|
|
9891
|
+
</diff>
|
|
9892
|
+
|
|
9893
|
+
`;
|
|
9894
|
+
if (recentCommits && recentCommits.length > 0) {
|
|
9895
|
+
const history = recentCommits.slice(0, 10).join("\n");
|
|
9896
|
+
prompt2 += `Recent commits on this branch (match style when appropriate):
|
|
9897
|
+
${history}
|
|
9898
|
+
|
|
9899
|
+
`;
|
|
9900
|
+
}
|
|
9901
|
+
if (hints?.split) {
|
|
9902
|
+
prompt2 += `MULTI-COMMIT MODE: If changes span multiple logical commits, focus the message on the primary change and mention other slices in the body.
|
|
9903
|
+
|
|
9904
|
+
`;
|
|
9905
|
+
}
|
|
9906
|
+
if (hints?.force_body) {
|
|
9907
|
+
prompt2 += `The user requires a BODY section after the subject line, even for small changes.
|
|
9908
|
+
|
|
9909
|
+
`;
|
|
9910
|
+
}
|
|
9911
|
+
if (rules && Object.keys(rules).length > 0) {
|
|
9912
|
+
prompt2 += `Rules: ${JSON.stringify(rules)}
|
|
9913
|
+
|
|
9914
|
+
`;
|
|
9915
|
+
}
|
|
9916
|
+
prompt2 += `Important:
|
|
9917
|
+
- Follow conventional commit format: <type>(<scope>): <subject>
|
|
9918
|
+
- Response should be the commit message only, no explanations`;
|
|
9919
|
+
return prompt2;
|
|
9920
|
+
}
|
|
9921
|
+
function buildRequest(provider, baseUrl, userContent, diff, changes, model, apiKey, rules, recentCommits, hints) {
|
|
9922
|
+
const headers = {
|
|
9923
|
+
"Content-Type": "application/json"
|
|
9305
9924
|
};
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
}
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
}
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9925
|
+
if (apiKey) {
|
|
9926
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
9927
|
+
}
|
|
9928
|
+
if (provider === "openrouter") {
|
|
9929
|
+
headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
|
|
9930
|
+
headers["X-Title"] = "qc - AI Commit Message Generator";
|
|
9931
|
+
}
|
|
9932
|
+
let url;
|
|
9933
|
+
let body;
|
|
9934
|
+
switch (provider) {
|
|
9935
|
+
case "ollama":
|
|
9936
|
+
url = `${baseUrl}/api/generate`;
|
|
9937
|
+
body = {
|
|
9938
|
+
model,
|
|
9939
|
+
prompt: userContent,
|
|
9940
|
+
stream: false,
|
|
9941
|
+
options: {}
|
|
9942
|
+
};
|
|
9943
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9944
|
+
case "lmstudio":
|
|
9945
|
+
url = `${baseUrl}/chat/completions`;
|
|
9946
|
+
body = {
|
|
9947
|
+
model,
|
|
9948
|
+
stream: false,
|
|
9949
|
+
messages: [
|
|
9950
|
+
{
|
|
9951
|
+
role: "system",
|
|
9952
|
+
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9953
|
+
},
|
|
9954
|
+
{ role: "user", content: userContent }
|
|
9955
|
+
]
|
|
9956
|
+
};
|
|
9957
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9958
|
+
case "openrouter":
|
|
9959
|
+
case "custom":
|
|
9960
|
+
url = `${baseUrl}/chat/completions`;
|
|
9961
|
+
body = {
|
|
9962
|
+
model,
|
|
9963
|
+
stream: false,
|
|
9964
|
+
messages: [
|
|
9965
|
+
{
|
|
9966
|
+
role: "system",
|
|
9967
|
+
content: "You are a git commit message generator. Create conventional commit messages."
|
|
9968
|
+
},
|
|
9969
|
+
{ role: "user", content: userContent }
|
|
9970
|
+
]
|
|
9971
|
+
};
|
|
9972
|
+
return { url, body, headers };
|
|
9973
|
+
case "cloudflare": {
|
|
9974
|
+
url = `${baseUrl.replace(/\/$/, "")}/commit`;
|
|
9975
|
+
const payload = { diff, changes, rules };
|
|
9976
|
+
if (recentCommits && recentCommits.length > 0) {
|
|
9977
|
+
payload.recent_commits = recentCommits.slice(0, 10);
|
|
9329
9978
|
}
|
|
9330
|
-
|
|
9979
|
+
if (hints && Object.keys(hints).length > 0) {
|
|
9980
|
+
payload.generation_hints = hints;
|
|
9981
|
+
}
|
|
9982
|
+
body = payload;
|
|
9983
|
+
return { url, body, headers: { "Content-Type": "application/json" } };
|
|
9984
|
+
}
|
|
9985
|
+
default:
|
|
9986
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
9331
9987
|
}
|
|
9332
|
-
const log = {
|
|
9333
|
-
step: (msg) => process.stderr.write(format.step(msg) + "\n"),
|
|
9334
|
-
success: (msg) => process.stderr.write(format.success(msg) + "\n"),
|
|
9335
|
-
error: (msg) => process.stderr.write(format.error(msg) + "\n"),
|
|
9336
|
-
dim: (msg) => process.stderr.write(format.dim(msg) + "\n")
|
|
9337
|
-
};
|
|
9338
|
-
return { isColor, format, spinner: createSpinner, log };
|
|
9339
9988
|
}
|
|
9340
|
-
function
|
|
9341
|
-
|
|
9342
|
-
|
|
9989
|
+
function parseResponse(provider, data) {
|
|
9990
|
+
const r = data;
|
|
9991
|
+
switch (provider) {
|
|
9992
|
+
case "ollama":
|
|
9993
|
+
return r.response ?? "";
|
|
9994
|
+
case "lmstudio":
|
|
9995
|
+
case "openrouter":
|
|
9996
|
+
case "custom": {
|
|
9997
|
+
const choices = r.choices;
|
|
9998
|
+
return choices?.[0]?.message?.content ?? "";
|
|
9999
|
+
}
|
|
10000
|
+
case "cloudflare":
|
|
10001
|
+
return r.commit?.response ?? "";
|
|
10002
|
+
default:
|
|
10003
|
+
return "";
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
async function runLocalCommit(args) {
|
|
10007
|
+
const silent = !!(args.hookMode || args.quiet);
|
|
10008
|
+
const ui2 = getUI();
|
|
10009
|
+
const log = silent ? createSilentLog() : ui2.log;
|
|
10010
|
+
if (!isGitRepo()) {
|
|
10011
|
+
throw new Error("Not a git repository.");
|
|
10012
|
+
}
|
|
10013
|
+
if (!hasStagedChanges()) {
|
|
10014
|
+
throw new Error("No staged changes. Stage files with `git add` first.");
|
|
10015
|
+
}
|
|
10016
|
+
const local = getLocalProviderConfig();
|
|
10017
|
+
if (!local) {
|
|
10018
|
+
throw new Error(
|
|
10019
|
+
"No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
|
|
10020
|
+
);
|
|
10021
|
+
}
|
|
10022
|
+
const config2 = getConfig();
|
|
10023
|
+
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
10024
|
+
let diff = getStagedDiff(excludes);
|
|
10025
|
+
const changes = getStagedFiles();
|
|
10026
|
+
if (!args.noSmartDiff) {
|
|
10027
|
+
const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
|
|
10028
|
+
diff = smartResult.processedDiff;
|
|
10029
|
+
if (smartResult.summarized.length > 0 && !silent) {
|
|
10030
|
+
log.step(
|
|
10031
|
+
`smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
|
|
10032
|
+
);
|
|
10033
|
+
}
|
|
10034
|
+
if (smartResult.aggressivelySummarized.length > 0 && !silent) {
|
|
10035
|
+
log.step(
|
|
10036
|
+
`large-diff: ${smartResult.aggressivelySummarized.length} additional file(s) summarized to fit (commit message may be less specific)`
|
|
10037
|
+
);
|
|
10038
|
+
}
|
|
10039
|
+
}
|
|
10040
|
+
let rules = { ...await detectCommitlintRules(), ...config2.rules ?? {} };
|
|
10041
|
+
const workspace = detectWorkspace();
|
|
10042
|
+
if (workspace) {
|
|
10043
|
+
const stagedFiles = changes.trim().split("\n").filter(Boolean);
|
|
10044
|
+
const scope = autoDetectScope(stagedFiles, workspace);
|
|
10045
|
+
if (scope) {
|
|
10046
|
+
const scopes = scope.split(",").map((s) => s.trim());
|
|
10047
|
+
rules = { ...rules, scopes };
|
|
10048
|
+
}
|
|
10049
|
+
}
|
|
10050
|
+
rules = applyCliTypeScopeToRules(rules, args.type, args.scope);
|
|
10051
|
+
const recentCommits = args.noContext ? void 0 : getRecentBranchCommits(5);
|
|
10052
|
+
const generationHints = generationHintsFromArgs(args.split, args.forceBody);
|
|
10053
|
+
const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
10054
|
+
const skipConfirm = args.dryRun || args.messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
10055
|
+
const model = args.model ?? local.model;
|
|
10056
|
+
const modelDisplay = model ?? local.model ?? "default";
|
|
10057
|
+
const userContent = buildUserPrompt(
|
|
10058
|
+
changes,
|
|
10059
|
+
diff,
|
|
10060
|
+
Object.keys(rules).length > 0 ? rules : void 0,
|
|
10061
|
+
recentCommits,
|
|
10062
|
+
generationHints
|
|
10063
|
+
);
|
|
10064
|
+
const { url, body, headers } = buildRequest(
|
|
10065
|
+
local.provider,
|
|
10066
|
+
local.baseUrl,
|
|
10067
|
+
userContent,
|
|
10068
|
+
diff,
|
|
10069
|
+
changes,
|
|
10070
|
+
model,
|
|
10071
|
+
local.apiKey,
|
|
10072
|
+
rules,
|
|
10073
|
+
recentCommits,
|
|
10074
|
+
generationHints
|
|
10075
|
+
);
|
|
10076
|
+
if (!url || url.includes("YOUR-WORKER")) {
|
|
10077
|
+
throw new Error(
|
|
10078
|
+
"Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
|
|
10079
|
+
);
|
|
10080
|
+
}
|
|
10081
|
+
const spinner = ui2.spinner(`generating commit (${modelDisplay} via ${local.provider})...`);
|
|
10082
|
+
if (!silent) spinner.start();
|
|
10083
|
+
const t0 = Date.now();
|
|
10084
|
+
let res;
|
|
10085
|
+
try {
|
|
10086
|
+
res = await fetch(url, {
|
|
10087
|
+
method: "POST",
|
|
10088
|
+
headers,
|
|
10089
|
+
body: JSON.stringify(body)
|
|
10090
|
+
});
|
|
10091
|
+
} finally {
|
|
10092
|
+
spinner.stop();
|
|
10093
|
+
}
|
|
10094
|
+
const roundTripMs = Date.now() - t0;
|
|
10095
|
+
if (!res.ok) {
|
|
10096
|
+
const text = await res.text();
|
|
10097
|
+
throw new Error(`Provider error (${res.status}): ${text}`);
|
|
10098
|
+
}
|
|
10099
|
+
const data = await res.json();
|
|
10100
|
+
let message = parseResponse(local.provider, data);
|
|
10101
|
+
message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
|
|
10102
|
+
if (!message) {
|
|
10103
|
+
throw new Error("Failed to generate commit message.");
|
|
10104
|
+
}
|
|
10105
|
+
const diagnostics = local.provider === "cloudflare" && typeof data === "object" && data !== null ? data.diagnostics : void 0;
|
|
10106
|
+
logVerboseDiagnostics((msg) => log.dim(msg), args.verbose, args.quiet, diagnostics, roundTripMs);
|
|
10107
|
+
if (args.interactive) {
|
|
10108
|
+
if (shouldSkipTTYInteraction(args.hookMode)) {
|
|
10109
|
+
if (!silent) log.dim("(--interactive ignored: not running in a TTY)");
|
|
10110
|
+
} else {
|
|
10111
|
+
const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
|
|
10112
|
+
if (refineResult.action === "abort") {
|
|
10113
|
+
return;
|
|
10114
|
+
}
|
|
10115
|
+
message = refineResult.message;
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
if (args.messageOnly) {
|
|
10119
|
+
console.log(message);
|
|
10120
|
+
return;
|
|
10121
|
+
}
|
|
10122
|
+
if (!silent) {
|
|
10123
|
+
const stagedPaths = changes.trim().split("\n").filter(Boolean);
|
|
10124
|
+
const short = getStagedDiffShortstat();
|
|
10125
|
+
const tokenEst = diagnostics && typeof diagnostics === "object" && diagnostics !== null && "tokenUsage" in diagnostics ? diagnostics.tokenUsage?.totalEstimated : void 0;
|
|
10126
|
+
displayCommitMessage(message, {
|
|
10127
|
+
log,
|
|
10128
|
+
isColor: ui2.isColor,
|
|
9343
10129
|
isTTY: !!process.stderr.isTTY,
|
|
9344
|
-
|
|
10130
|
+
style: "rich",
|
|
10131
|
+
stagedFiles: stagedPaths,
|
|
10132
|
+
stats: {
|
|
10133
|
+
files: stagedPaths.length,
|
|
10134
|
+
additions: short.additions,
|
|
10135
|
+
deletions: short.deletions,
|
|
10136
|
+
...tokenEst !== void 0 ? { tokens: tokenEst } : {}
|
|
10137
|
+
}
|
|
9345
10138
|
});
|
|
9346
10139
|
}
|
|
9347
|
-
|
|
10140
|
+
if (args.dryRun) {
|
|
10141
|
+
return;
|
|
10142
|
+
}
|
|
10143
|
+
if (args.confirm) {
|
|
10144
|
+
const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
|
|
10145
|
+
if (confirmResult.action === "abort") {
|
|
10146
|
+
return;
|
|
10147
|
+
}
|
|
10148
|
+
}
|
|
10149
|
+
gitCommit(message);
|
|
10150
|
+
const branch = getCurrentBranch();
|
|
10151
|
+
log.step(`[${branch} committed]`);
|
|
10152
|
+
if (args.push) {
|
|
10153
|
+
const pushStats = getPushStats();
|
|
10154
|
+
log.step(`pushing to origin/${branch}...`);
|
|
10155
|
+
gitPush();
|
|
10156
|
+
if (pushStats) {
|
|
10157
|
+
log.success(`pushed ${pushStats.commits} commit(s) \xB7 ${pushStats.stat}`);
|
|
10158
|
+
} else {
|
|
10159
|
+
log.success("pushed");
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
9348
10162
|
}
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
10163
|
+
async function generateLocalBranchName(opts) {
|
|
10164
|
+
const local = getLocalProviderConfig();
|
|
10165
|
+
if (!local) {
|
|
10166
|
+
throw new Error("No local provider configured. Set provider with `qc --use-ollama` etc.");
|
|
10167
|
+
}
|
|
10168
|
+
const sections = [];
|
|
10169
|
+
sections.push("Generate a git branch name in the format <type>/<kebab-case-slug>.");
|
|
10170
|
+
sections.push("Type must be one of: feat, fix, refactor, perf, docs, test, chore, ci.");
|
|
10171
|
+
sections.push("Slug: 2-5 words, lowercase, hyphen-separated, max 55 chars.");
|
|
10172
|
+
sections.push("Output ONLY the branch name on a single line. No explanation.");
|
|
10173
|
+
sections.push("");
|
|
10174
|
+
if (opts.description) {
|
|
10175
|
+
sections.push("DESCRIPTION:");
|
|
10176
|
+
sections.push(opts.description);
|
|
10177
|
+
} else if (opts.recentCommits && opts.recentCommits.length > 0) {
|
|
10178
|
+
sections.push("RECENT COMMITS:");
|
|
10179
|
+
for (const c of opts.recentCommits) sections.push(`- ${c}`);
|
|
10180
|
+
} else if (opts.diff) {
|
|
10181
|
+
sections.push("DIFF:");
|
|
10182
|
+
sections.push(opts.diff.slice(0, 3e4));
|
|
10183
|
+
}
|
|
10184
|
+
const userContent = sections.join("\n");
|
|
10185
|
+
const model = opts.model ?? local.model;
|
|
10186
|
+
const headers = { "Content-Type": "application/json" };
|
|
10187
|
+
if (local.apiKey) headers.Authorization = `Bearer ${local.apiKey}`;
|
|
10188
|
+
if (local.provider === "openrouter") {
|
|
10189
|
+
headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
|
|
10190
|
+
headers["X-Title"] = "qc - AI Commit Message Generator";
|
|
10191
|
+
}
|
|
10192
|
+
let url;
|
|
10193
|
+
let body;
|
|
10194
|
+
switch (local.provider) {
|
|
10195
|
+
case "ollama":
|
|
10196
|
+
url = `${local.baseUrl}/api/generate`;
|
|
10197
|
+
body = { model, prompt: userContent, stream: false, options: {} };
|
|
10198
|
+
break;
|
|
10199
|
+
case "lmstudio":
|
|
10200
|
+
case "openrouter":
|
|
10201
|
+
case "custom":
|
|
10202
|
+
url = `${local.baseUrl}/chat/completions`;
|
|
10203
|
+
body = {
|
|
10204
|
+
model,
|
|
10205
|
+
stream: false,
|
|
10206
|
+
messages: [
|
|
10207
|
+
{
|
|
10208
|
+
role: "system",
|
|
10209
|
+
content: "You suggest concise git branch names. Reply with the branch name only."
|
|
10210
|
+
},
|
|
10211
|
+
{ role: "user", content: userContent }
|
|
10212
|
+
]
|
|
10213
|
+
};
|
|
10214
|
+
break;
|
|
10215
|
+
case "cloudflare":
|
|
10216
|
+
url = `${local.baseUrl.replace(/\/$/, "")}/branch`;
|
|
10217
|
+
body = {
|
|
10218
|
+
diff: opts.diff,
|
|
10219
|
+
changes: opts.changes,
|
|
10220
|
+
recent_commits: opts.recentCommits,
|
|
10221
|
+
description: opts.description,
|
|
10222
|
+
model,
|
|
10223
|
+
cf_model: model,
|
|
10224
|
+
...opts.rules ? { rules: opts.rules } : {}
|
|
10225
|
+
};
|
|
10226
|
+
break;
|
|
10227
|
+
}
|
|
10228
|
+
if (!url || url.includes("YOUR-WORKER")) {
|
|
10229
|
+
throw new Error(
|
|
10230
|
+
"Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
|
|
10231
|
+
);
|
|
10232
|
+
}
|
|
10233
|
+
let res;
|
|
10234
|
+
try {
|
|
10235
|
+
res = await fetch(url, {
|
|
10236
|
+
method: "POST",
|
|
10237
|
+
headers,
|
|
10238
|
+
body: JSON.stringify(body)
|
|
10239
|
+
});
|
|
10240
|
+
} catch {
|
|
10241
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10242
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
10243
|
+
}
|
|
10244
|
+
if (!res.ok) {
|
|
10245
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10246
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
10247
|
+
}
|
|
10248
|
+
const data = await res.json();
|
|
10249
|
+
let raw;
|
|
10250
|
+
if (local.provider === "cloudflare") {
|
|
10251
|
+
const r = data;
|
|
10252
|
+
const br = r.branch;
|
|
10253
|
+
raw = typeof br?.name === "string" ? br.name : "";
|
|
10254
|
+
} else if (local.provider === "ollama") {
|
|
10255
|
+
raw = data.response ?? "";
|
|
10256
|
+
} else {
|
|
10257
|
+
const choices = data.choices;
|
|
10258
|
+
raw = choices?.[0]?.message?.content ?? "";
|
|
10259
|
+
}
|
|
10260
|
+
raw = raw.replace(/[\r\n].*$/s, "").trim();
|
|
10261
|
+
const sanitized = sanitizeBranchName(raw);
|
|
10262
|
+
if (!sanitized) {
|
|
10263
|
+
const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
|
|
10264
|
+
return ensureUniqueName(fallback.name, branchExists);
|
|
10265
|
+
}
|
|
10266
|
+
return ensureUniqueName(sanitized, branchExists);
|
|
10267
|
+
}
|
|
10268
|
+
async function runLocalBranch(opts) {
|
|
10269
|
+
const local = getLocalProviderConfig();
|
|
10270
|
+
if (!local) {
|
|
10271
|
+
throw new Error("No local provider configured. Set provider with `qc --use-ollama` etc.");
|
|
10272
|
+
}
|
|
10273
|
+
const ui2 = getUI();
|
|
10274
|
+
const log = ui2.log;
|
|
10275
|
+
const spinner = ui2.spinner(`generating branch name (${opts.model ?? local.model} via ${local.provider})...`);
|
|
10276
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10277
|
+
let final;
|
|
10278
|
+
try {
|
|
10279
|
+
final = await generateLocalBranchName({
|
|
10280
|
+
description: opts.description,
|
|
10281
|
+
diff: opts.diff,
|
|
10282
|
+
changes: opts.changes,
|
|
10283
|
+
recentCommits: opts.recentCommits,
|
|
10284
|
+
model: opts.model,
|
|
10285
|
+
rules: opts.rules
|
|
10286
|
+
});
|
|
10287
|
+
} catch {
|
|
10288
|
+
const filesArr = opts.changes?.split("\n").filter(Boolean) ?? [];
|
|
10289
|
+
const fallback = deterministicBranchName({ files: filesArr, description: opts.description });
|
|
10290
|
+
final = ensureUniqueName(fallback.name, branchExists);
|
|
10291
|
+
log.dim("(used local fallback name; AI generation failed)");
|
|
10292
|
+
} finally {
|
|
10293
|
+
spinner.stop();
|
|
10294
|
+
}
|
|
10295
|
+
log.success(`branch name: ${final}`);
|
|
10296
|
+
const baseRef = opts.baseRef ?? "HEAD";
|
|
10297
|
+
if (opts.noSwitch) {
|
|
10298
|
+
createBranch(final, baseRef);
|
|
10299
|
+
log.success(`created ${final} (not switched)`);
|
|
10300
|
+
} else {
|
|
10301
|
+
createAndCheckoutBranch(final, baseRef);
|
|
10302
|
+
log.success(`switched to ${final}`);
|
|
10303
|
+
}
|
|
10304
|
+
if (opts.push) {
|
|
10305
|
+
gitPushSetUpstream(final);
|
|
10306
|
+
log.success(`pushed origin/${final}`);
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10309
|
+
var import_fs8, import_path8, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
|
|
10310
|
+
var init_local = __esm({
|
|
10311
|
+
"src/local.ts"() {
|
|
9352
10312
|
"use strict";
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
10313
|
+
import_fs8 = require("fs");
|
|
10314
|
+
import_path8 = require("path");
|
|
10315
|
+
import_os4 = require("os");
|
|
10316
|
+
init_config();
|
|
10317
|
+
init_dist();
|
|
10318
|
+
init_git();
|
|
10319
|
+
init_monorepo();
|
|
10320
|
+
init_commitlint();
|
|
10321
|
+
init_smart_diff();
|
|
10322
|
+
init_ui();
|
|
10323
|
+
init_commit_helpers();
|
|
10324
|
+
init_branch_name();
|
|
10325
|
+
CONFIG_PATH2 = (0, import_path8.join)((0, import_os4.homedir)(), CONFIG_DIR);
|
|
10326
|
+
PROVIDER_URLS = {
|
|
10327
|
+
ollama: "http://localhost:11434",
|
|
10328
|
+
lmstudio: "http://localhost:1234/v1",
|
|
10329
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
10330
|
+
custom: "",
|
|
10331
|
+
cloudflare: ""
|
|
10332
|
+
};
|
|
10333
|
+
DEFAULT_MODELS = {
|
|
10334
|
+
ollama: "codellama",
|
|
10335
|
+
lmstudio: "default",
|
|
10336
|
+
openrouter: "google/gemini-flash-1.5-8b",
|
|
10337
|
+
custom: "",
|
|
10338
|
+
cloudflare: "@cf/qwen/qwen2.5-coder-32b-instruct"
|
|
10339
|
+
};
|
|
10340
|
+
}
|
|
10341
|
+
});
|
|
10342
|
+
|
|
10343
|
+
// src/commands/branch.ts
|
|
10344
|
+
var branch_exports = {};
|
|
10345
|
+
__export(branch_exports, {
|
|
10346
|
+
runBranch: () => runBranch
|
|
10347
|
+
});
|
|
10348
|
+
function branchGenerationRules(cfg) {
|
|
10349
|
+
const types = cfg.branch?.generation?.types;
|
|
10350
|
+
if (types && types.length > 0) return { types: [...types] };
|
|
10351
|
+
return void 0;
|
|
10352
|
+
}
|
|
10353
|
+
function finalizeGeneratedBranchName(raw) {
|
|
10354
|
+
return finalizeBranchName(raw, branchExists);
|
|
10355
|
+
}
|
|
10356
|
+
async function runBranch(opts) {
|
|
10357
|
+
const ui2 = getUI();
|
|
10358
|
+
const log = ui2.log;
|
|
10359
|
+
if (!isGitRepo()) {
|
|
10360
|
+
log.error("Not a git repository.");
|
|
10361
|
+
process.exit(1);
|
|
10362
|
+
}
|
|
10363
|
+
const baseRef = opts.from ?? "HEAD";
|
|
10364
|
+
const config2 = getConfig();
|
|
10365
|
+
const model = opts.model ?? config2.model;
|
|
10366
|
+
const genRules = branchGenerationRules(config2);
|
|
10367
|
+
if (opts.rescue) {
|
|
10368
|
+
const state = detectProtectedBranchState({
|
|
10369
|
+
protectedBranches: config2.branch?.protectedBranches,
|
|
10370
|
+
detectDefault: config2.branch?.detectDefault
|
|
10371
|
+
});
|
|
10372
|
+
if (!state.isProtected) {
|
|
10373
|
+
throw new Error(
|
|
10374
|
+
"`--rescue` only applies on a protected branch (e.g. main). The current branch is not protected."
|
|
10375
|
+
);
|
|
10376
|
+
}
|
|
10377
|
+
if (state.commitsAhead === 0) {
|
|
10378
|
+
throw new Error(
|
|
10379
|
+
"No commits ahead of upstream to rescue. Push your branch or use `qc branch` without `--rescue`."
|
|
10380
|
+
);
|
|
10381
|
+
}
|
|
10382
|
+
let final2;
|
|
10383
|
+
if (opts.explicitName) {
|
|
10384
|
+
const sanitized = sanitizeBranchName(opts.explicitName);
|
|
10385
|
+
if (!sanitized) {
|
|
10386
|
+
throw new Error(`invalid branch name: ${opts.explicitName}`);
|
|
10387
|
+
}
|
|
10388
|
+
final2 = finalizeBranchName(sanitized, branchExists);
|
|
10389
|
+
} else {
|
|
10390
|
+
const recent = getRecentBranchCommits(state.commitsAhead);
|
|
10391
|
+
const apiKey2 = opts.apiKey ?? getApiKey();
|
|
10392
|
+
if (apiKey2) {
|
|
10393
|
+
const spinner2 = ui2.spinner(`generating branch name (${model ?? "default"})...`);
|
|
10394
|
+
if (process.stderr.isTTY) spinner2.start();
|
|
10395
|
+
try {
|
|
10396
|
+
const client = new ApiClient({ apiKey: apiKey2 });
|
|
10397
|
+
try {
|
|
10398
|
+
const result2 = await client.generateBranchName({
|
|
10399
|
+
recent_commits: recent,
|
|
10400
|
+
model: opts.model,
|
|
10401
|
+
description: opts.message,
|
|
10402
|
+
rules: genRules
|
|
10403
|
+
});
|
|
10404
|
+
final2 = finalizeGeneratedBranchName(result2.name);
|
|
10405
|
+
} catch {
|
|
10406
|
+
const fallback = deterministicBranchName({
|
|
10407
|
+
description: recent.join(" ") || opts.message
|
|
10408
|
+
});
|
|
10409
|
+
final2 = finalizeBranchName(fallback.name, branchExists);
|
|
10410
|
+
log.dim("(used deterministic fallback name; API generation failed)");
|
|
10411
|
+
}
|
|
10412
|
+
} finally {
|
|
10413
|
+
spinner2.stop();
|
|
10414
|
+
}
|
|
10415
|
+
} else {
|
|
10416
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, generateLocalBranchName: generateLocalBranchName2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10417
|
+
if (!getLocalProviderConfig2()) {
|
|
10418
|
+
throw new Error(
|
|
10419
|
+
"Not authenticated. Run `qc login` first, or configure a local provider for `--rescue`."
|
|
10420
|
+
);
|
|
10421
|
+
}
|
|
10422
|
+
const spinner2 = ui2.spinner(`generating branch name (${model ?? "default"} via local)...`);
|
|
10423
|
+
if (process.stderr.isTTY) spinner2.start();
|
|
10424
|
+
try {
|
|
10425
|
+
try {
|
|
10426
|
+
const name = await generateLocalBranchName2({
|
|
10427
|
+
recentCommits: recent,
|
|
10428
|
+
model: opts.model,
|
|
10429
|
+
description: opts.message,
|
|
10430
|
+
rules: genRules
|
|
10431
|
+
});
|
|
10432
|
+
final2 = finalizeBranchName(name, branchExists, { skipUniqueness: true });
|
|
10433
|
+
} catch {
|
|
10434
|
+
const fallback = deterministicBranchName({
|
|
10435
|
+
description: recent.join(" ") || opts.message
|
|
10436
|
+
});
|
|
10437
|
+
final2 = finalizeBranchName(fallback.name, branchExists);
|
|
10438
|
+
log.dim("(used deterministic fallback name; local provider failed)");
|
|
10439
|
+
}
|
|
10440
|
+
} finally {
|
|
10441
|
+
spinner2.stop();
|
|
10442
|
+
}
|
|
9358
10443
|
}
|
|
9359
|
-
}
|
|
10444
|
+
}
|
|
10445
|
+
log.success(`branch name: ${final2}`);
|
|
10446
|
+
if (opts.dryRun) {
|
|
10447
|
+
log.dim("(dry-run; not running rescue)");
|
|
10448
|
+
return;
|
|
10449
|
+
}
|
|
10450
|
+
if (!process.stdin.isTTY) {
|
|
10451
|
+
throw new Error("`--rescue` requires an interactive terminal to confirm (or use `qc branch <name>` after arranging commits manually).");
|
|
10452
|
+
}
|
|
10453
|
+
log.dim(
|
|
10454
|
+
`About to: 1) create ${final2} at HEAD, 2) reset ${state.branch} to upstream, 3) switch to ${final2}`
|
|
10455
|
+
);
|
|
10456
|
+
if (!await promptYesNo("Continue with rescue?")) {
|
|
10457
|
+
log.dim("aborted.");
|
|
10458
|
+
return;
|
|
10459
|
+
}
|
|
10460
|
+
rescueCommits({ currentBranch: state.branch, newBranch: final2 });
|
|
10461
|
+
log.success(`moved ${state.commitsAhead} commit(s) to ${final2}`);
|
|
10462
|
+
log.success(`${state.branch} reset to upstream`);
|
|
10463
|
+
if (opts.push) {
|
|
10464
|
+
gitPushSetUpstream(final2);
|
|
10465
|
+
log.success(`pushed origin/${final2}`);
|
|
10466
|
+
}
|
|
10467
|
+
return;
|
|
9360
10468
|
}
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
10469
|
+
if (opts.explicitName) {
|
|
10470
|
+
const sanitized = sanitizeBranchName(opts.explicitName);
|
|
10471
|
+
if (!sanitized) {
|
|
10472
|
+
throw new Error(`invalid branch name: ${opts.explicitName}`);
|
|
10473
|
+
}
|
|
10474
|
+
const final2 = finalizeBranchName(sanitized, branchExists);
|
|
10475
|
+
if (opts.dryRun) {
|
|
10476
|
+
log.success(`would create branch: ${final2}`);
|
|
10477
|
+
return;
|
|
10478
|
+
}
|
|
10479
|
+
if (opts.noSwitch) {
|
|
10480
|
+
createBranch(final2, baseRef);
|
|
10481
|
+
log.success(`created branch ${final2} (not switched)`);
|
|
10482
|
+
} else {
|
|
10483
|
+
createAndCheckoutBranch(final2, baseRef);
|
|
10484
|
+
log.success(`switched to ${final2}`);
|
|
10485
|
+
}
|
|
10486
|
+
if (opts.push) {
|
|
10487
|
+
gitPushSetUpstream(final2);
|
|
10488
|
+
log.success(`pushed origin/${final2}`);
|
|
10489
|
+
}
|
|
10490
|
+
return;
|
|
9368
10491
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
10492
|
+
const payload = { model, rules: genRules };
|
|
10493
|
+
if (opts.message) {
|
|
10494
|
+
payload.description = opts.message;
|
|
10495
|
+
} else if (opts.fromCommits) {
|
|
10496
|
+
payload.recent_commits = getRecentBranchCommits(10);
|
|
10497
|
+
} else {
|
|
10498
|
+
if (!hasStagedChanges()) {
|
|
10499
|
+
throw new Error(
|
|
10500
|
+
"No staged changes detected. Stage with `git add`, or provide -m '<description>'."
|
|
10501
|
+
);
|
|
10502
|
+
}
|
|
10503
|
+
payload.diff = getStagedDiff(config2.excludes ?? []);
|
|
10504
|
+
payload.changes = getStagedFiles();
|
|
9371
10505
|
}
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
10506
|
+
const apiKey = opts.apiKey ?? getApiKey();
|
|
10507
|
+
if (!apiKey) {
|
|
10508
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, runLocalBranch: runLocalBranch2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10509
|
+
if (getLocalProviderConfig2()) {
|
|
10510
|
+
await runLocalBranch2({
|
|
10511
|
+
description: opts.message,
|
|
10512
|
+
diff: opts.message ? void 0 : payload.diff,
|
|
10513
|
+
changes: opts.message ? void 0 : payload.changes,
|
|
10514
|
+
recentCommits: payload.recent_commits,
|
|
10515
|
+
model: opts.model,
|
|
10516
|
+
noSwitch: opts.noSwitch,
|
|
10517
|
+
push: opts.push,
|
|
10518
|
+
baseRef,
|
|
10519
|
+
rules: genRules
|
|
10520
|
+
});
|
|
10521
|
+
return;
|
|
10522
|
+
}
|
|
10523
|
+
throw new Error("Not authenticated. Run `qc login` first, or provide --message.");
|
|
9387
10524
|
}
|
|
9388
|
-
const
|
|
9389
|
-
if (
|
|
9390
|
-
|
|
10525
|
+
const spinner = ui2.spinner(`generating branch name (${model ?? "default"})...`);
|
|
10526
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10527
|
+
let result;
|
|
10528
|
+
try {
|
|
10529
|
+
const client = new ApiClient({ apiKey });
|
|
10530
|
+
result = await client.generateBranchName(payload);
|
|
10531
|
+
} finally {
|
|
10532
|
+
spinner.stop();
|
|
9391
10533
|
}
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
10534
|
+
const final = finalizeGeneratedBranchName(result.name);
|
|
10535
|
+
log.success(`branch name: ${final}`);
|
|
10536
|
+
if (opts.dryRun) {
|
|
10537
|
+
log.dim(`(dry-run; not creating)`);
|
|
10538
|
+
return;
|
|
10539
|
+
}
|
|
10540
|
+
if (opts.noSwitch) {
|
|
10541
|
+
createBranch(final, baseRef);
|
|
10542
|
+
log.success(`created ${final} (not switched)`);
|
|
10543
|
+
} else {
|
|
10544
|
+
createAndCheckoutBranch(final, baseRef);
|
|
10545
|
+
log.success(`switched to ${final}`);
|
|
10546
|
+
}
|
|
10547
|
+
if (opts.push) {
|
|
10548
|
+
gitPushSetUpstream(final);
|
|
10549
|
+
log.success(`pushed origin/${final}`);
|
|
9401
10550
|
}
|
|
9402
|
-
return lines.join("\n");
|
|
9403
10551
|
}
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
10552
|
+
var init_branch2 = __esm({
|
|
10553
|
+
"src/commands/branch.ts"() {
|
|
10554
|
+
"use strict";
|
|
10555
|
+
init_api();
|
|
10556
|
+
init_config();
|
|
10557
|
+
init_branch_rescue();
|
|
10558
|
+
init_protected_branch_guard();
|
|
10559
|
+
init_git();
|
|
10560
|
+
init_branch_name();
|
|
10561
|
+
init_commit_helpers();
|
|
10562
|
+
init_ui();
|
|
10563
|
+
}
|
|
10564
|
+
});
|
|
9410
10565
|
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
10566
|
+
// src/commands/init.ts
|
|
10567
|
+
var init_exports = {};
|
|
10568
|
+
__export(init_exports, {
|
|
10569
|
+
init: () => init
|
|
10570
|
+
});
|
|
10571
|
+
function init(options) {
|
|
10572
|
+
let hooksDir;
|
|
10573
|
+
try {
|
|
10574
|
+
hooksDir = (0, import_child_process6.execFileSync)("git", ["rev-parse", "--git-path", "hooks"], {
|
|
10575
|
+
encoding: "utf-8"
|
|
10576
|
+
}).trim();
|
|
10577
|
+
} catch {
|
|
10578
|
+
console.error("Error: Not a git repository");
|
|
10579
|
+
process.exit(1);
|
|
10580
|
+
}
|
|
10581
|
+
const hookPath = (0, import_path9.join)(hooksDir, "prepare-commit-msg");
|
|
10582
|
+
if (options.uninstall) {
|
|
10583
|
+
if ((0, import_fs9.existsSync)(hookPath)) {
|
|
10584
|
+
const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
|
|
10585
|
+
if (content.includes("Quikcommit")) {
|
|
10586
|
+
(0, import_fs9.unlinkSync)(hookPath);
|
|
10587
|
+
console.log("Quikcommit hook removed.");
|
|
10588
|
+
} else {
|
|
10589
|
+
console.log("Hook exists but was not installed by Quikcommit. Skipping.");
|
|
9423
10590
|
}
|
|
9424
|
-
|
|
9425
|
-
|
|
10591
|
+
} else {
|
|
10592
|
+
console.log("No hook to remove.");
|
|
9426
10593
|
}
|
|
9427
|
-
return
|
|
9428
|
-
} finally {
|
|
9429
|
-
rl.close();
|
|
10594
|
+
return;
|
|
9430
10595
|
}
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
const ans = (await rl.question(prompt2)).trim().toLowerCase();
|
|
9437
|
-
if (ans !== "y" && ans !== "yes") {
|
|
9438
|
-
return { action: "abort" };
|
|
10596
|
+
if ((0, import_fs9.existsSync)(hookPath)) {
|
|
10597
|
+
const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
|
|
10598
|
+
if (content.includes("Quikcommit")) {
|
|
10599
|
+
console.log("Quikcommit hook is already installed.");
|
|
10600
|
+
return;
|
|
9439
10601
|
}
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
10602
|
+
console.error(
|
|
10603
|
+
"A prepare-commit-msg hook already exists. Use --uninstall first or manually merge."
|
|
10604
|
+
);
|
|
10605
|
+
process.exit(1);
|
|
9443
10606
|
}
|
|
10607
|
+
(0, import_fs9.writeFileSync)(hookPath, HOOK_CONTENT);
|
|
10608
|
+
(0, import_fs9.chmodSync)(hookPath, 493);
|
|
10609
|
+
console.log("Quikcommit hook installed.");
|
|
10610
|
+
console.log("Now just run `git commit` and a message will be generated automatically.");
|
|
9444
10611
|
}
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
10612
|
+
var import_fs9, import_path9, import_child_process6, HOOK_CONTENT;
|
|
10613
|
+
var init_init = __esm({
|
|
10614
|
+
"src/commands/init.ts"() {
|
|
10615
|
+
"use strict";
|
|
10616
|
+
import_fs9 = require("fs");
|
|
10617
|
+
import_path9 = require("path");
|
|
10618
|
+
import_child_process6 = require("child_process");
|
|
10619
|
+
HOOK_CONTENT = `#!/bin/sh
|
|
10620
|
+
# Quikcommit - auto-generate commit messages
|
|
10621
|
+
# Installed by: qc init
|
|
10622
|
+
# Remove with: qc init --uninstall
|
|
10623
|
+
|
|
10624
|
+
# Only generate if no message was provided (empty commit message file)
|
|
10625
|
+
COMMIT_MSG_FILE="$1"
|
|
10626
|
+
COMMIT_SOURCE="$2"
|
|
10627
|
+
|
|
10628
|
+
# Skip if message was provided via -m, merge, squash, etc.
|
|
10629
|
+
if [ -n "$COMMIT_SOURCE" ]; then
|
|
10630
|
+
exit 0
|
|
10631
|
+
fi
|
|
10632
|
+
|
|
10633
|
+
# Skip if message file already has content (excluding comments)
|
|
10634
|
+
if grep -qv '^#' "$COMMIT_MSG_FILE" 2>/dev/null; then
|
|
10635
|
+
if [ -n "$(grep -v '^#' "$COMMIT_MSG_FILE" | grep -v '^$')" ]; then
|
|
10636
|
+
exit 0
|
|
10637
|
+
fi
|
|
10638
|
+
fi
|
|
10639
|
+
|
|
10640
|
+
# Generate commit message
|
|
10641
|
+
MSG=$(qc --message-only --hook-mode 2>/dev/null)
|
|
10642
|
+
if [ $? -eq 0 ] && [ -n "$MSG" ]; then
|
|
10643
|
+
printf '%s
|
|
10644
|
+
' "$MSG" > "$COMMIT_MSG_FILE"
|
|
10645
|
+
fi
|
|
10646
|
+
`;
|
|
10647
|
+
}
|
|
10648
|
+
});
|
|
10649
|
+
|
|
10650
|
+
// src/commands/team.ts
|
|
10651
|
+
var team_exports = {};
|
|
10652
|
+
__export(team_exports, {
|
|
10653
|
+
team: () => team
|
|
10654
|
+
});
|
|
10655
|
+
function createApiClient() {
|
|
10656
|
+
return new ApiClient();
|
|
9456
10657
|
}
|
|
9457
|
-
function
|
|
9458
|
-
return
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
}
|
|
10658
|
+
function mapCommitlintToRules(config2) {
|
|
10659
|
+
if (!config2 || typeof config2 !== "object") return null;
|
|
10660
|
+
const c = config2;
|
|
10661
|
+
const rules = {};
|
|
10662
|
+
const _ext = c.extends;
|
|
10663
|
+
const rulesConfig = c.rules;
|
|
10664
|
+
if (Array.isArray(rulesConfig?.["type-enum"]) && rulesConfig["type-enum"].length >= 3) {
|
|
10665
|
+
const [, , value] = rulesConfig["type-enum"];
|
|
10666
|
+
if (Array.isArray(value)) rules.types = value;
|
|
10667
|
+
}
|
|
10668
|
+
if (Array.isArray(rulesConfig?.["scope-enum"]) && rulesConfig["scope-enum"].length >= 3) {
|
|
10669
|
+
const [, , value] = rulesConfig["scope-enum"];
|
|
10670
|
+
if (Array.isArray(value)) rules.scopes = value;
|
|
10671
|
+
}
|
|
10672
|
+
if (Array.isArray(rulesConfig?.["header-max-length"]) && rulesConfig["header-max-length"].length >= 3) {
|
|
10673
|
+
const [, , maxLen] = rulesConfig["header-max-length"];
|
|
10674
|
+
if (typeof maxLen === "number") rules.headerMaxLength = maxLen;
|
|
10675
|
+
}
|
|
10676
|
+
if (Array.isArray(rulesConfig?.["subject-case"]) && rulesConfig["subject-case"].length >= 3) {
|
|
10677
|
+
const [, , val] = rulesConfig["subject-case"];
|
|
10678
|
+
if (val != null) rules.subjectCase = Array.isArray(val) ? val : [val];
|
|
10679
|
+
}
|
|
10680
|
+
return Object.keys(rules).length > 0 ? rules : null;
|
|
9467
10681
|
}
|
|
9468
|
-
function
|
|
9469
|
-
const
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
10682
|
+
function detectLocalCommitlintRules() {
|
|
10683
|
+
const cwd = process.cwd();
|
|
10684
|
+
const files = [
|
|
10685
|
+
".commitlintrc.json",
|
|
10686
|
+
".commitlintrc",
|
|
10687
|
+
"commitlint.config.js",
|
|
10688
|
+
"commitlint.config.cjs",
|
|
10689
|
+
"commitlint.config.mjs"
|
|
10690
|
+
];
|
|
10691
|
+
for (const file of files) {
|
|
10692
|
+
const path = (0, import_path10.join)(cwd, file);
|
|
10693
|
+
if (!(0, import_fs10.existsSync)(path)) continue;
|
|
10694
|
+
try {
|
|
10695
|
+
const content = (0, import_fs10.readFileSync)(path, "utf-8");
|
|
10696
|
+
let parsed;
|
|
10697
|
+
if (file.endsWith(".json") || file === ".commitlintrc") {
|
|
10698
|
+
parsed = JSON.parse(content);
|
|
10699
|
+
} else {
|
|
10700
|
+
continue;
|
|
10701
|
+
}
|
|
10702
|
+
const rules = mapCommitlintToRules(parsed);
|
|
10703
|
+
if (rules) return rules;
|
|
10704
|
+
} catch {
|
|
9474
10705
|
}
|
|
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
10706
|
}
|
|
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;
|
|
10707
|
+
const pkgPath = (0, import_path10.join)(cwd, "package.json");
|
|
10708
|
+
if ((0, import_fs10.existsSync)(pkgPath)) {
|
|
10709
|
+
try {
|
|
10710
|
+
const content = (0, import_fs10.readFileSync)(pkgPath, "utf-8");
|
|
10711
|
+
const pkg = JSON.parse(content);
|
|
10712
|
+
if (pkg.commitlint) {
|
|
10713
|
+
const rules = mapCommitlintToRules(pkg.commitlint);
|
|
10714
|
+
if (rules) return rules;
|
|
9499
10715
|
}
|
|
10716
|
+
} catch {
|
|
9500
10717
|
}
|
|
9501
|
-
}
|
|
10718
|
+
}
|
|
10719
|
+
const config2 = getConfig();
|
|
10720
|
+
if (config2.rules && Object.keys(config2.rules).length > 0) {
|
|
10721
|
+
return config2.rules;
|
|
9502
10722
|
}
|
|
9503
10723
|
return null;
|
|
9504
10724
|
}
|
|
9505
|
-
function
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
10725
|
+
async function team(subcommand, args) {
|
|
10726
|
+
const api = createApiClient();
|
|
10727
|
+
switch (subcommand) {
|
|
10728
|
+
case void 0:
|
|
10729
|
+
case "info": {
|
|
10730
|
+
const info = await api.getTeam();
|
|
10731
|
+
console.log(`
|
|
10732
|
+
Team: ${info.name}`);
|
|
10733
|
+
console.log(` Plan: ${info.plan}`);
|
|
10734
|
+
console.log(` Members: ${info.member_count}`);
|
|
10735
|
+
console.log("\n Members:");
|
|
10736
|
+
for (const m of info.members) {
|
|
10737
|
+
console.log(` ${m.name ?? m.email} <${m.email}> (${m.role})`);
|
|
10738
|
+
}
|
|
10739
|
+
break;
|
|
9510
10740
|
}
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
10741
|
+
case "rules": {
|
|
10742
|
+
if (args?.[0] === "push") {
|
|
10743
|
+
const rules = detectLocalCommitlintRules();
|
|
10744
|
+
if (!rules) {
|
|
10745
|
+
console.error("No local commitlint config found.");
|
|
10746
|
+
process.exit(1);
|
|
10747
|
+
}
|
|
10748
|
+
await api.pushTeamRules(rules);
|
|
10749
|
+
console.log("Team rules updated from local commitlint config.");
|
|
10750
|
+
} else {
|
|
10751
|
+
const rules = await api.getTeamRules();
|
|
10752
|
+
console.log("\n Team Commit Rules:");
|
|
10753
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
10754
|
+
}
|
|
10755
|
+
break;
|
|
9521
10756
|
}
|
|
9522
|
-
|
|
10757
|
+
case "invite": {
|
|
10758
|
+
const email = args?.[0];
|
|
10759
|
+
if (!email) {
|
|
10760
|
+
console.error("Usage: qc team invite <email>");
|
|
10761
|
+
process.exit(1);
|
|
10762
|
+
}
|
|
10763
|
+
await api.inviteTeamMember(email);
|
|
10764
|
+
console.log(`Invitation sent to ${email}`);
|
|
10765
|
+
break;
|
|
10766
|
+
}
|
|
10767
|
+
default:
|
|
10768
|
+
console.error(`Unknown team command: ${subcommand}`);
|
|
10769
|
+
console.log("Usage: qc team [info|rules|rules push|invite <email>]");
|
|
10770
|
+
process.exit(1);
|
|
9523
10771
|
}
|
|
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
10772
|
}
|
|
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
|
-
`;
|
|
10773
|
+
var import_fs10, import_path10;
|
|
10774
|
+
var init_team = __esm({
|
|
10775
|
+
"src/commands/team.ts"() {
|
|
10776
|
+
"use strict";
|
|
10777
|
+
import_fs10 = require("fs");
|
|
10778
|
+
import_path10 = require("path");
|
|
10779
|
+
init_api();
|
|
10780
|
+
init_config();
|
|
9557
10781
|
}
|
|
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.
|
|
10782
|
+
});
|
|
9560
10783
|
|
|
9561
|
-
|
|
10784
|
+
// src/commands/config.ts
|
|
10785
|
+
var config_exports2 = {};
|
|
10786
|
+
__export(config_exports2, {
|
|
10787
|
+
config: () => config
|
|
10788
|
+
});
|
|
10789
|
+
function config(args) {
|
|
10790
|
+
if (args.length === 0) {
|
|
10791
|
+
showConfig();
|
|
10792
|
+
return;
|
|
9562
10793
|
}
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
10794
|
+
const sub = args[0];
|
|
10795
|
+
if (sub === "set") {
|
|
10796
|
+
const key = args[1];
|
|
10797
|
+
const value = args[2];
|
|
10798
|
+
if (!key || !value) {
|
|
10799
|
+
console.error("Usage: qc config set <key> <value>");
|
|
10800
|
+
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
10801
|
+
process.exit(1);
|
|
10802
|
+
}
|
|
10803
|
+
setConfig(key, value);
|
|
10804
|
+
return;
|
|
9567
10805
|
}
|
|
9568
|
-
if (
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
`;
|
|
10806
|
+
if (sub === "reset") {
|
|
10807
|
+
resetConfig();
|
|
10808
|
+
return;
|
|
9572
10809
|
}
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
return prompt2;
|
|
10810
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
10811
|
+
console.error("Usage: qc config [set <key> <value> | reset]");
|
|
10812
|
+
process.exit(1);
|
|
9577
10813
|
}
|
|
9578
|
-
function
|
|
9579
|
-
const
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
}
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
10814
|
+
function showConfig() {
|
|
10815
|
+
const cfg = getConfig();
|
|
10816
|
+
const apiKey = getApiKey();
|
|
10817
|
+
console.log("Current configuration:");
|
|
10818
|
+
console.log(` model: ${cfg.model ?? "(default for plan)"}`);
|
|
10819
|
+
console.log(` api_url: ${cfg.apiUrl ?? DEFAULT_API_URL}`);
|
|
10820
|
+
console.log(` provider: ${cfg.provider ?? "(default)"}`);
|
|
10821
|
+
console.log(` auto_stage: ${cfg.autoStage ? "true" : "false"}`);
|
|
10822
|
+
console.log(` auth: ${apiKey ? "****" : "not set"}`);
|
|
10823
|
+
if (cfg.excludes?.length) {
|
|
10824
|
+
console.log(` excludes: ${cfg.excludes.join(", ")}`);
|
|
9588
10825
|
}
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
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" } };
|
|
10826
|
+
}
|
|
10827
|
+
function setConfig(key, value) {
|
|
10828
|
+
const cfg = getConfig();
|
|
10829
|
+
const updates = {};
|
|
10830
|
+
if (key === "model") {
|
|
10831
|
+
updates.model = value;
|
|
10832
|
+
} else if (key === "provider") {
|
|
10833
|
+
const valid = ["ollama", "lmstudio", "openrouter", "custom", "cloudflare"];
|
|
10834
|
+
if (!valid.includes(value.toLowerCase())) {
|
|
10835
|
+
console.error(`Invalid provider. Must be one of: ${valid.join(", ")}`);
|
|
10836
|
+
process.exit(1);
|
|
10837
|
+
}
|
|
10838
|
+
updates.provider = value.toLowerCase();
|
|
10839
|
+
} else if (key === "api_url") {
|
|
10840
|
+
try {
|
|
10841
|
+
new URL(value);
|
|
10842
|
+
updates.apiUrl = value;
|
|
10843
|
+
} catch {
|
|
10844
|
+
console.error("Invalid URL:", value);
|
|
10845
|
+
process.exit(1);
|
|
9641
10846
|
}
|
|
9642
|
-
|
|
9643
|
-
|
|
10847
|
+
} else if (key === "auto_stage") {
|
|
10848
|
+
updates.autoStage = value === "true" || value === "1";
|
|
10849
|
+
} else {
|
|
10850
|
+
console.error(`Unknown key: ${key}`);
|
|
10851
|
+
console.error(" Keys: model, api_url, provider, auto_stage");
|
|
10852
|
+
process.exit(1);
|
|
9644
10853
|
}
|
|
10854
|
+
saveConfig({ ...cfg, ...updates });
|
|
10855
|
+
console.log(`Set ${key} = ${value}`);
|
|
9645
10856
|
}
|
|
9646
|
-
function
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
10857
|
+
function resetConfig() {
|
|
10858
|
+
saveConfig({});
|
|
10859
|
+
console.log("Config reset to defaults.");
|
|
10860
|
+
}
|
|
10861
|
+
var init_config2 = __esm({
|
|
10862
|
+
"src/commands/config.ts"() {
|
|
10863
|
+
"use strict";
|
|
10864
|
+
init_config();
|
|
10865
|
+
init_dist();
|
|
10866
|
+
}
|
|
10867
|
+
});
|
|
10868
|
+
|
|
10869
|
+
// src/commands/upgrade.ts
|
|
10870
|
+
var upgrade_exports = {};
|
|
10871
|
+
__export(upgrade_exports, {
|
|
10872
|
+
upgrade: () => upgrade
|
|
10873
|
+
});
|
|
10874
|
+
async function upgrade() {
|
|
10875
|
+
console.log(`
|
|
10876
|
+
Opening ${BILLING_URL}
|
|
10877
|
+
`);
|
|
10878
|
+
try {
|
|
10879
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
10880
|
+
if (process.platform === "darwin") {
|
|
10881
|
+
execFileSync7("open", [BILLING_URL]);
|
|
10882
|
+
} else if (process.platform === "linux") {
|
|
10883
|
+
execFileSync7("xdg-open", [BILLING_URL]);
|
|
10884
|
+
} else if (process.platform === "win32") {
|
|
10885
|
+
execFileSync7("cmd", ["/c", "start", "", BILLING_URL]);
|
|
9656
10886
|
}
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
default:
|
|
9660
|
-
return "";
|
|
10887
|
+
} catch {
|
|
10888
|
+
console.log(`Visit: ${BILLING_URL}`);
|
|
9661
10889
|
}
|
|
9662
10890
|
}
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
}
|
|
9669
|
-
if (!hasStagedChanges()) {
|
|
9670
|
-
throw new Error("No staged changes. Stage files with `git add` first.");
|
|
10891
|
+
var BILLING_URL;
|
|
10892
|
+
var init_upgrade = __esm({
|
|
10893
|
+
"src/commands/upgrade.ts"() {
|
|
10894
|
+
"use strict";
|
|
10895
|
+
BILLING_URL = "https://app.quikcommit.dev/billing";
|
|
9671
10896
|
}
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
10897
|
+
});
|
|
10898
|
+
|
|
10899
|
+
// src/branch-guard.ts
|
|
10900
|
+
async function runBranchGuard(args, log) {
|
|
10901
|
+
if (!shouldRunGuard({
|
|
10902
|
+
allowProtected: !!args.allowProtected,
|
|
10903
|
+
hookMode: !!args.hookMode,
|
|
10904
|
+
isTTY: !!process.stdin.isTTY
|
|
10905
|
+
})) {
|
|
10906
|
+
return { action: "continue" };
|
|
10907
|
+
}
|
|
10908
|
+
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
10909
|
+
const config2 = getConfig2();
|
|
10910
|
+
const state = detectProtectedBranchState({
|
|
10911
|
+
protectedBranches: config2.branch?.protectedBranches,
|
|
10912
|
+
detectDefault: config2.branch?.detectDefault
|
|
10913
|
+
});
|
|
10914
|
+
if (!state.isProtected) {
|
|
10915
|
+
return { action: "continue" };
|
|
9677
10916
|
}
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
10917
|
+
log.error(
|
|
10918
|
+
`You're on ${state.branch} (a protected branch).` + (state.commitsAhead > 0 ? ` ${state.commitsAhead} commit(s) ahead of upstream.` : "")
|
|
10919
|
+
);
|
|
10920
|
+
let action;
|
|
10921
|
+
let usedConfigDefault = false;
|
|
10922
|
+
if (args.autoBranch) {
|
|
10923
|
+
action = "branch";
|
|
10924
|
+
} else if (config2.branch?.defaultAction === "branch") {
|
|
10925
|
+
action = "branch";
|
|
10926
|
+
usedConfigDefault = true;
|
|
10927
|
+
} else if (config2.branch?.defaultAction === "continue") {
|
|
10928
|
+
action = "continue";
|
|
10929
|
+
usedConfigDefault = true;
|
|
10930
|
+
} else {
|
|
10931
|
+
action = await promptProtectedAction(state.mode);
|
|
9690
10932
|
}
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
if (workspace) {
|
|
9694
|
-
const stagedFiles = changes.trim().split("\n").filter(Boolean);
|
|
9695
|
-
const scope = autoDetectScope(stagedFiles, workspace);
|
|
9696
|
-
if (scope) {
|
|
9697
|
-
const scopes = scope.split(",").map((s) => s.trim());
|
|
9698
|
-
rules = { ...rules, scopes };
|
|
9699
|
-
}
|
|
10933
|
+
if (action === "continue" && usedConfigDefault) {
|
|
10934
|
+
log.dim("(continuing on protected branch per config `branch.defaultAction`)");
|
|
9700
10935
|
}
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9705
|
-
const skipConfirm = args.dryRun || args.messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9706
|
-
const model = args.model ?? local.model;
|
|
9707
|
-
const modelDisplay = model ?? local.model ?? "default";
|
|
9708
|
-
const userContent = buildUserPrompt(
|
|
9709
|
-
changes,
|
|
9710
|
-
diff,
|
|
9711
|
-
Object.keys(rules).length > 0 ? rules : void 0,
|
|
9712
|
-
recentCommits,
|
|
9713
|
-
generationHints
|
|
9714
|
-
);
|
|
9715
|
-
const { url, body, headers } = buildRequest(
|
|
9716
|
-
local.provider,
|
|
9717
|
-
local.baseUrl,
|
|
9718
|
-
userContent,
|
|
9719
|
-
diff,
|
|
9720
|
-
changes,
|
|
9721
|
-
model,
|
|
9722
|
-
local.apiKey,
|
|
9723
|
-
rules,
|
|
9724
|
-
recentCommits,
|
|
9725
|
-
generationHints
|
|
9726
|
-
);
|
|
9727
|
-
if (!url || url.includes("YOUR-WORKER")) {
|
|
9728
|
-
throw new Error(
|
|
9729
|
-
"Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
|
|
9730
|
-
);
|
|
10936
|
+
if (action === "abort") {
|
|
10937
|
+
log.dim("aborted.");
|
|
10938
|
+
return { action: "abort" };
|
|
9731
10939
|
}
|
|
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();
|
|
10940
|
+
if (action === "continue") {
|
|
10941
|
+
return { action: "continue" };
|
|
9744
10942
|
}
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
10943
|
+
let stagedDiff = "";
|
|
10944
|
+
let stagedChanges = "";
|
|
10945
|
+
if (state.mode === "uncommitted") {
|
|
10946
|
+
stagedDiff = getStagedDiff(args.excludes ?? []);
|
|
10947
|
+
stagedChanges = getStagedFiles();
|
|
10948
|
+
if (!stagedDiff.trim()) {
|
|
10949
|
+
stagedDiff = getWorkingTreeDiff(args.excludes ?? []);
|
|
10950
|
+
stagedChanges = getAllChangedFiles();
|
|
10951
|
+
}
|
|
9749
10952
|
}
|
|
9750
|
-
const
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
10953
|
+
const recentCommits = state.mode === "rescue" ? getRecentBranchCommits(state.commitsAhead) : void 0;
|
|
10954
|
+
const branchRules = args.branchRules ?? (config2.branch?.generation?.types && config2.branch.generation.types.length > 0 ? { types: [...config2.branch.generation.types] } : void 0);
|
|
10955
|
+
const apiKey = args.apiKey;
|
|
10956
|
+
const ui2 = getUI();
|
|
10957
|
+
let generateLocalBranchNameFn;
|
|
10958
|
+
if (!apiKey) {
|
|
10959
|
+
const { getLocalProviderConfig: getLocalProviderConfig2, generateLocalBranchName: generateLocalBranchName2 } = await Promise.resolve().then(() => (init_local(), local_exports));
|
|
10960
|
+
if (!getLocalProviderConfig2()) {
|
|
10961
|
+
log.error(
|
|
10962
|
+
"Cannot generate branch name: not authenticated and no local provider configured. Run `qc login` or configure a local provider."
|
|
10963
|
+
);
|
|
10964
|
+
return { action: "abort" };
|
|
10965
|
+
}
|
|
10966
|
+
generateLocalBranchNameFn = generateLocalBranchName2;
|
|
9755
10967
|
}
|
|
9756
|
-
const
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
10968
|
+
const spinner = ui2.spinner(`generating branch name...`);
|
|
10969
|
+
if (process.stderr.isTTY) spinner.start();
|
|
10970
|
+
let rawName;
|
|
10971
|
+
let usedFallback = false;
|
|
10972
|
+
try {
|
|
10973
|
+
if (apiKey) {
|
|
10974
|
+
const client = new ApiClient({ apiKey });
|
|
10975
|
+
try {
|
|
10976
|
+
const branchResult = await client.generateBranchName({
|
|
10977
|
+
diff: stagedDiff || void 0,
|
|
10978
|
+
changes: stagedChanges || void 0,
|
|
10979
|
+
recent_commits: recentCommits,
|
|
10980
|
+
model: args.model,
|
|
10981
|
+
rules: branchRules
|
|
10982
|
+
});
|
|
10983
|
+
rawName = branchResult.name;
|
|
10984
|
+
} catch {
|
|
10985
|
+
const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
|
|
10986
|
+
rawName = deterministicBranchName(fallbackInput).name;
|
|
10987
|
+
usedFallback = true;
|
|
10988
|
+
}
|
|
9761
10989
|
} else {
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
10990
|
+
try {
|
|
10991
|
+
rawName = await generateLocalBranchNameFn({
|
|
10992
|
+
diff: stagedDiff || void 0,
|
|
10993
|
+
changes: stagedChanges || void 0,
|
|
10994
|
+
recentCommits,
|
|
10995
|
+
model: args.model,
|
|
10996
|
+
rules: branchRules
|
|
10997
|
+
});
|
|
10998
|
+
} catch {
|
|
10999
|
+
const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
|
|
11000
|
+
rawName = deterministicBranchName(fallbackInput).name;
|
|
11001
|
+
usedFallback = true;
|
|
9765
11002
|
}
|
|
9766
|
-
message = refineResult.message;
|
|
9767
11003
|
}
|
|
11004
|
+
} finally {
|
|
11005
|
+
spinner.stop();
|
|
9768
11006
|
}
|
|
9769
|
-
if (
|
|
9770
|
-
|
|
9771
|
-
return;
|
|
9772
|
-
}
|
|
9773
|
-
if (!silent) {
|
|
9774
|
-
displayCommitMessage(message, log);
|
|
9775
|
-
}
|
|
9776
|
-
if (args.dryRun) {
|
|
9777
|
-
return;
|
|
11007
|
+
if (usedFallback) {
|
|
11008
|
+
log.dim("(used local fallback name; AI generation failed)");
|
|
9778
11009
|
}
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
11010
|
+
let final;
|
|
11011
|
+
try {
|
|
11012
|
+
final = finalizeBranchName(rawName, branchExists);
|
|
11013
|
+
} catch {
|
|
11014
|
+
const generatorName = usedFallback ? "deterministic fallback" : apiKey ? "API generator" : "local provider";
|
|
11015
|
+
log.error(`Invalid branch name from ${generatorName}: ${rawName}`);
|
|
11016
|
+
return { action: "abort" };
|
|
11017
|
+
}
|
|
11018
|
+
log.success(`branch name: ${final}`);
|
|
11019
|
+
if (state.mode === "rescue") {
|
|
11020
|
+
log.dim(
|
|
11021
|
+
`About to: 1) create ${final} at HEAD, 2) reset ${state.branch} to upstream, 3) switch to ${final}`
|
|
11022
|
+
);
|
|
11023
|
+
const confirmed = await promptYesNo("Continue with rescue?");
|
|
11024
|
+
if (!confirmed) {
|
|
11025
|
+
log.dim("aborted.");
|
|
11026
|
+
return { action: "abort" };
|
|
9783
11027
|
}
|
|
11028
|
+
try {
|
|
11029
|
+
rescueCommits({ currentBranch: state.branch, newBranch: final });
|
|
11030
|
+
log.success(`moved ${state.commitsAhead} commit(s) to ${final}`);
|
|
11031
|
+
log.success(`${state.branch} reset to upstream`);
|
|
11032
|
+
} catch (err) {
|
|
11033
|
+
log.error(`Rescue failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11034
|
+
return { action: "abort" };
|
|
11035
|
+
}
|
|
11036
|
+
return { action: "done" };
|
|
9784
11037
|
}
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
11038
|
+
createAndCheckoutBranch(final);
|
|
11039
|
+
log.success(`switched to ${final}`);
|
|
11040
|
+
return { action: "continue" };
|
|
11041
|
+
}
|
|
11042
|
+
async function promptProtectedAction(mode) {
|
|
11043
|
+
const rl = import_promises2.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
11044
|
+
try {
|
|
11045
|
+
const continueLabel = mode === "rescue" ? "commit on this branch anyway (do not move existing commits)" : "commit on this branch anyway (not recommended)";
|
|
11046
|
+
const branchLabel = mode === "rescue" ? "create a new branch and move your existing commits to it" : "create a new branch first, then commit your staged changes there";
|
|
11047
|
+
process.stderr.write(
|
|
11048
|
+
`
|
|
11049
|
+
What would you like to do?
|
|
11050
|
+
(b)ranch ${branchLabel} \u2190 default
|
|
11051
|
+
(c)ommit ${continueLabel}
|
|
11052
|
+
(a)bort cancel without committing
|
|
11053
|
+
`
|
|
11054
|
+
);
|
|
11055
|
+
const answer = (await rl.question("> ")).trim().toLowerCase();
|
|
11056
|
+
if (answer === "" || answer === "b" || answer === "branch" || answer === "y") return "branch";
|
|
11057
|
+
if (answer === "c" || answer === "commit") return "continue";
|
|
11058
|
+
if (answer === "a" || answer === "abort" || answer === "n") return "abort";
|
|
11059
|
+
process.stderr.write(`(unrecognized response "${answer}" \u2014 aborting)
|
|
11060
|
+
`);
|
|
11061
|
+
return "abort";
|
|
11062
|
+
} finally {
|
|
11063
|
+
rl.close();
|
|
9788
11064
|
}
|
|
9789
11065
|
}
|
|
9790
|
-
var
|
|
9791
|
-
var
|
|
9792
|
-
"src/
|
|
11066
|
+
var import_promises2;
|
|
11067
|
+
var init_branch_guard = __esm({
|
|
11068
|
+
"src/branch-guard.ts"() {
|
|
9793
11069
|
"use strict";
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
import_os4 = require("os");
|
|
9797
|
-
init_config();
|
|
9798
|
-
init_dist();
|
|
11070
|
+
import_promises2 = __toESM(require("node:readline/promises"));
|
|
11071
|
+
init_api();
|
|
9799
11072
|
init_git();
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
11073
|
+
init_protected_branch_guard();
|
|
11074
|
+
init_branch_rescue();
|
|
11075
|
+
init_branch_name();
|
|
9803
11076
|
init_ui();
|
|
9804
11077
|
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
11078
|
}
|
|
9821
11079
|
});
|
|
9822
11080
|
|
|
@@ -9828,23 +11086,42 @@ __export(commit_exports, {
|
|
|
9828
11086
|
async function runCommit(args) {
|
|
9829
11087
|
const { messageOnly, push, apiKey: apiKeyFlag, hookMode, model: modelFlag, all } = args;
|
|
9830
11088
|
const silent = !!(hookMode || args.quiet);
|
|
9831
|
-
const
|
|
11089
|
+
const ui2 = getUI();
|
|
11090
|
+
const log = silent ? createSilentLog() : ui2.log;
|
|
9832
11091
|
if (!isGitRepo()) {
|
|
9833
11092
|
log.error("Not a git repository.");
|
|
9834
11093
|
process.exit(1);
|
|
9835
11094
|
}
|
|
9836
11095
|
const config2 = getConfig();
|
|
11096
|
+
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
11097
|
+
const guardResult = await runBranchGuard(
|
|
11098
|
+
{
|
|
11099
|
+
allowProtected: !!(args.allowProtected || config2.branch?.allowProtected),
|
|
11100
|
+
autoBranch: !!args.autoBranch,
|
|
11101
|
+
hookMode: !!args.hookMode,
|
|
11102
|
+
apiKey: apiKeyFlag ?? getApiKey() ?? void 0,
|
|
11103
|
+
model: args.model,
|
|
11104
|
+
excludes
|
|
11105
|
+
},
|
|
11106
|
+
log
|
|
11107
|
+
);
|
|
11108
|
+
if (guardResult.action === "abort") {
|
|
11109
|
+
return;
|
|
11110
|
+
}
|
|
11111
|
+
if (guardResult.action === "done") {
|
|
11112
|
+
return;
|
|
11113
|
+
}
|
|
11114
|
+
const _exhaustive = guardResult.action;
|
|
11115
|
+
void _exhaustive;
|
|
9837
11116
|
if (all || config2.autoStage) {
|
|
9838
11117
|
stageAll();
|
|
9839
|
-
const
|
|
9840
|
-
const fileList = total > 3 ? `${files.join(", ")}, +${total - 3} more` : files.join(", ");
|
|
11118
|
+
const total = getStagedFileCount();
|
|
9841
11119
|
log.step(`staging working tree (${total} file(s))...`);
|
|
9842
|
-
if (fileList) log.dim(` ${fileList}`);
|
|
9843
11120
|
}
|
|
9844
11121
|
if (!hasStagedChanges()) {
|
|
9845
11122
|
const unstaged = getUnstagedFiles();
|
|
9846
11123
|
if (unstaged.length > 0) {
|
|
9847
|
-
log.error("No staged changes. Use `qc -a` to stage
|
|
11124
|
+
log.error("No staged changes. Use `qc -a` to stage all files (modified + untracked), or `git add` manually.");
|
|
9848
11125
|
} else {
|
|
9849
11126
|
log.error("No changes to commit.");
|
|
9850
11127
|
}
|
|
@@ -9856,18 +11133,22 @@ async function runCommit(args) {
|
|
|
9856
11133
|
process.exit(1);
|
|
9857
11134
|
}
|
|
9858
11135
|
const model = modelFlag ?? config2.model;
|
|
9859
|
-
const excludes = [...config2.excludes ?? [], ...args.exclude];
|
|
9860
11136
|
const diff = getStagedDiff(excludes);
|
|
9861
11137
|
const changes = getStagedFiles();
|
|
9862
11138
|
let processedDiff = diff;
|
|
9863
11139
|
if (!args.noSmartDiff) {
|
|
9864
|
-
const smartResult =
|
|
11140
|
+
const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
|
|
9865
11141
|
processedDiff = smartResult.processedDiff;
|
|
9866
11142
|
if (smartResult.summarized.length > 0) {
|
|
9867
11143
|
log.step(
|
|
9868
11144
|
`smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
|
|
9869
11145
|
);
|
|
9870
11146
|
}
|
|
11147
|
+
if (smartResult.aggressivelySummarized.length > 0) {
|
|
11148
|
+
log.step(
|
|
11149
|
+
`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)`
|
|
11150
|
+
);
|
|
11151
|
+
}
|
|
9871
11152
|
}
|
|
9872
11153
|
const commitlintRules = await detectCommitlintRules();
|
|
9873
11154
|
let rules = { ...commitlintRules, ...config2.rules ?? {} };
|
|
@@ -9901,7 +11182,7 @@ async function runCommit(args) {
|
|
|
9901
11182
|
const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9902
11183
|
const skipConfirm = args.dryRun || messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
|
|
9903
11184
|
const modelDisplay = model ?? "default";
|
|
9904
|
-
const spinner =
|
|
11185
|
+
const spinner = ui2.spinner(`generating commit (${modelDisplay})...`);
|
|
9905
11186
|
if (!silent) spinner.start();
|
|
9906
11187
|
const t0 = Date.now();
|
|
9907
11188
|
let generatedMessage;
|
|
@@ -9927,7 +11208,7 @@ async function runCommit(args) {
|
|
|
9927
11208
|
} else {
|
|
9928
11209
|
const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
|
|
9929
11210
|
if (refineResult.action === "abort") {
|
|
9930
|
-
|
|
11211
|
+
return;
|
|
9931
11212
|
}
|
|
9932
11213
|
message = refineResult.message;
|
|
9933
11214
|
}
|
|
@@ -9936,14 +11217,29 @@ async function runCommit(args) {
|
|
|
9936
11217
|
console.log(message);
|
|
9937
11218
|
return;
|
|
9938
11219
|
}
|
|
9939
|
-
|
|
11220
|
+
const stagedPaths = changes.trim().split("\n").filter(Boolean);
|
|
11221
|
+
const short = getStagedDiffShortstat();
|
|
11222
|
+
const tokenEst = diagnostics && typeof diagnostics === "object" && diagnostics !== null && "tokenUsage" in diagnostics ? diagnostics.tokenUsage?.totalEstimated : void 0;
|
|
11223
|
+
displayCommitMessage(message, {
|
|
11224
|
+
log,
|
|
11225
|
+
isColor: ui2.isColor,
|
|
11226
|
+
isTTY: !!process.stderr.isTTY,
|
|
11227
|
+
style: "rich",
|
|
11228
|
+
stagedFiles: stagedPaths,
|
|
11229
|
+
stats: {
|
|
11230
|
+
files: stagedPaths.length,
|
|
11231
|
+
additions: short.additions,
|
|
11232
|
+
deletions: short.deletions,
|
|
11233
|
+
...tokenEst !== void 0 ? { tokens: tokenEst } : {}
|
|
11234
|
+
}
|
|
11235
|
+
});
|
|
9940
11236
|
if (args.dryRun) {
|
|
9941
11237
|
return;
|
|
9942
11238
|
}
|
|
9943
11239
|
if (args.confirm) {
|
|
9944
11240
|
const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
|
|
9945
11241
|
if (confirmResult.action === "abort") {
|
|
9946
|
-
|
|
11242
|
+
return;
|
|
9947
11243
|
}
|
|
9948
11244
|
}
|
|
9949
11245
|
gitCommit(message);
|
|
@@ -9951,11 +11247,11 @@ async function runCommit(args) {
|
|
|
9951
11247
|
const branch = getCurrentBranch();
|
|
9952
11248
|
log.step(`[${branch} ${hash}] committed`);
|
|
9953
11249
|
if (push) {
|
|
11250
|
+
const pushStats = getPushStats();
|
|
9954
11251
|
log.step(`pushing to origin/${branch}...`);
|
|
9955
11252
|
gitPush();
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
log.success(`pushed ${stats.commits} commit(s) \xB7 ${stats.stat}`);
|
|
11253
|
+
if (pushStats) {
|
|
11254
|
+
log.success(`pushed ${pushStats.commits} commit(s) \xB7 ${pushStats.stat}`);
|
|
9959
11255
|
} else {
|
|
9960
11256
|
log.success("pushed");
|
|
9961
11257
|
}
|
|
@@ -9972,6 +11268,7 @@ var init_commit = __esm({
|
|
|
9972
11268
|
init_ui();
|
|
9973
11269
|
init_smart_diff();
|
|
9974
11270
|
init_commit_helpers();
|
|
11271
|
+
init_branch_guard();
|
|
9975
11272
|
}
|
|
9976
11273
|
});
|
|
9977
11274
|
|
|
@@ -9989,6 +11286,7 @@ Usage:
|
|
|
9989
11286
|
qc pr Generate PR description from branch commits
|
|
9990
11287
|
qc changelog Generate changelog from commits since last tag
|
|
9991
11288
|
qc changeset Automate pnpm changeset with AI
|
|
11289
|
+
qc branch Generate branch name + create branch (use --message for description)
|
|
9992
11290
|
qc init Install prepare-commit-msg hook
|
|
9993
11291
|
qc login Sign in via browser
|
|
9994
11292
|
qc logout Clear local credentials
|
|
@@ -10018,11 +11316,22 @@ Flags:
|
|
|
10018
11316
|
--model <id> Use specific model
|
|
10019
11317
|
--base <branch> Base branch for pr/changeset (default: main)
|
|
10020
11318
|
--create Create PR with gh CLI (qc pr --create)
|
|
10021
|
-
--from <ref> Start ref for changelog
|
|
11319
|
+
--from <ref> Start ref for changelog / base ref for qc branch
|
|
10022
11320
|
--to <ref> End ref for changelog
|
|
10023
11321
|
--write Write changelog to CHANGELOG.md
|
|
10024
11322
|
--hook-mode Silent mode for git hooks
|
|
10025
11323
|
|
|
11324
|
+
Branch flags (qc branch):
|
|
11325
|
+
--message <text> Generate from a description (no diff needed)
|
|
11326
|
+
--from-commits Generate from recent commits instead of diff
|
|
11327
|
+
--rescue Move commits off current protected branch (see docs)
|
|
11328
|
+
--no-switch Create branch but don't checkout
|
|
11329
|
+
--from <ref> Create branch from this ref (default: HEAD)
|
|
11330
|
+
|
|
11331
|
+
Commit guard flags:
|
|
11332
|
+
--allow-protected Bypass protected-branch guard for this run
|
|
11333
|
+
--auto-branch Auto-create branch with generated name (no prompt)
|
|
11334
|
+
|
|
10026
11335
|
Compose short flags: qc -ap (stage all + push), qc -apv (+ verbose)
|
|
10027
11336
|
|
|
10028
11337
|
Examples:
|
|
@@ -10133,10 +11442,18 @@ function parseArgs(args) {
|
|
|
10133
11442
|
result.command = "help";
|
|
10134
11443
|
} else if (arg === "--all") {
|
|
10135
11444
|
result.all = true;
|
|
11445
|
+
} else if (arg === "--allow-protected") {
|
|
11446
|
+
result.allowProtected = true;
|
|
11447
|
+
} else if (arg === "--auto-branch") {
|
|
11448
|
+
result.autoBranch = true;
|
|
10136
11449
|
} else if (arg === "--message-only") {
|
|
10137
11450
|
result.messageOnly = true;
|
|
11451
|
+
} else if (arg === "--message" && i + 1 < args.length) {
|
|
11452
|
+
result.message = args[++i];
|
|
10138
11453
|
} else if (arg === "--push") {
|
|
10139
11454
|
result.push = true;
|
|
11455
|
+
} else if (arg === "--rescue") {
|
|
11456
|
+
result.rescue = true;
|
|
10140
11457
|
} else if (arg === "--verbose") {
|
|
10141
11458
|
result.verbose = true;
|
|
10142
11459
|
} else if (arg === "--quiet") {
|
|
@@ -10157,6 +11474,8 @@ function parseArgs(args) {
|
|
|
10157
11474
|
result.noContext = true;
|
|
10158
11475
|
} else if (arg === "--no-smart-diff") {
|
|
10159
11476
|
result.noSmartDiff = true;
|
|
11477
|
+
} else if (arg === "--no-switch") {
|
|
11478
|
+
result.noSwitch = true;
|
|
10160
11479
|
} else if (arg === "--local" || arg === "--use-ollama" || arg === "--use-lmstudio" || arg === "--use-openrouter" || arg === "--use-cloudflare") {
|
|
10161
11480
|
result.local = true;
|
|
10162
11481
|
if (arg === "--use-ollama") {
|
|
@@ -10176,6 +11495,8 @@ function parseArgs(args) {
|
|
|
10176
11495
|
result.create = true;
|
|
10177
11496
|
} else if (arg === "--from" && i + 1 < args.length) {
|
|
10178
11497
|
result.from = args[++i];
|
|
11498
|
+
} else if (arg === "--from-commits") {
|
|
11499
|
+
result.fromCommits = true;
|
|
10179
11500
|
} else if (arg === "--to" && i + 1 < args.length) {
|
|
10180
11501
|
result.to = args[++i];
|
|
10181
11502
|
} else if (arg === "--write") {
|
|
@@ -10211,6 +11532,9 @@ function parseArgs(args) {
|
|
|
10211
11532
|
} else if (arg === "changelog") {
|
|
10212
11533
|
result.command = "changelog";
|
|
10213
11534
|
subcommandSeen = true;
|
|
11535
|
+
} else if (arg === "branch") {
|
|
11536
|
+
result.command = "branch";
|
|
11537
|
+
subcommandSeen = true;
|
|
10214
11538
|
} else if (arg === "init") {
|
|
10215
11539
|
result.command = "init";
|
|
10216
11540
|
subcommandSeen = true;
|
|
@@ -10325,6 +11649,23 @@ async function main() {
|
|
|
10325
11649
|
});
|
|
10326
11650
|
return;
|
|
10327
11651
|
}
|
|
11652
|
+
if (command === "branch") {
|
|
11653
|
+
const { runBranch: runBranch2 } = await Promise.resolve().then(() => (init_branch2(), branch_exports));
|
|
11654
|
+
const explicitName = values.positionals[0];
|
|
11655
|
+
await runBranch2({
|
|
11656
|
+
explicitName,
|
|
11657
|
+
message: values.message,
|
|
11658
|
+
fromCommits: values.fromCommits,
|
|
11659
|
+
rescue: values.rescue,
|
|
11660
|
+
dryRun: values.dryRun,
|
|
11661
|
+
noSwitch: values.noSwitch,
|
|
11662
|
+
push: values.push,
|
|
11663
|
+
from: values.from,
|
|
11664
|
+
model: values.model,
|
|
11665
|
+
apiKey: values.apiKey
|
|
11666
|
+
});
|
|
11667
|
+
return;
|
|
11668
|
+
}
|
|
10328
11669
|
if (command === "init") {
|
|
10329
11670
|
const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
10330
11671
|
init2({ uninstall: values.uninstall });
|
|
@@ -10336,7 +11677,7 @@ async function main() {
|
|
|
10336
11677
|
return;
|
|
10337
11678
|
}
|
|
10338
11679
|
if (command === "config") {
|
|
10339
|
-
const { config: config2 } = await Promise.resolve().then(() => (init_config2(),
|
|
11680
|
+
const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
10340
11681
|
config2(values.positionals);
|
|
10341
11682
|
return;
|
|
10342
11683
|
}
|