@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.
Files changed (2) hide show
  1. package/dist/index.js +2191 -850
  2. 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).filter((line) => !line.startsWith("??"));
7912
+ return output.trim().split("\n").filter(Boolean);
7807
7913
  }
7808
7914
  function stageAll() {
7809
- (0, import_child_process2.execFileSync)("git", ["add", "-u"], { stdio: "pipe" });
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: "inherit" });
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
- (0, import_child_process2.execFileSync)("git", ["push"], { stdio: "inherit" });
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 getShortStagedFiles(max = 3) {
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
- const all = output.trim().split("\n").filter(Boolean);
7901
- return { files: all.slice(0, max), total: all.length };
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/commands/init.ts
8766
- var init_exports = {};
8767
- __export(init_exports, {
8768
- init: () => init
8769
- });
8770
- function init(options) {
8771
- let hooksDir;
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 hookPath = (0, import_path8.join)(hooksDir, "prepare-commit-msg");
8781
- if (options.uninstall) {
8782
- if ((0, import_fs8.existsSync)(hookPath)) {
8783
- const content = (0, import_fs8.readFileSync)(hookPath, "utf-8");
8784
- if (content.includes("Quikcommit")) {
8785
- (0, import_fs8.unlinkSync)(hookPath);
8786
- console.log("Quikcommit hook removed.");
8787
- } else {
8788
- console.log("Hook exists but was not installed by Quikcommit. Skipping.");
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
- return;
9021
+ throw err;
8794
9022
  }
8795
- if ((0, import_fs8.existsSync)(hookPath)) {
8796
- const content = (0, import_fs8.readFileSync)(hookPath, "utf-8");
8797
- if (content.includes("Quikcommit")) {
8798
- console.log("Quikcommit hook is already installed.");
8799
- return;
9023
+ try {
9024
+ resetHard(upstream);
9025
+ } catch (err) {
9026
+ try {
9027
+ deleteBranch(opts.newBranch);
9028
+ } catch {
8800
9029
  }
8801
- console.error(
8802
- "A prepare-commit-msg hook already exists. Use --uninstall first or manually merge."
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
- (0, import_fs8.writeFileSync)(hookPath, HOOK_CONTENT);
8807
- (0, import_fs8.chmodSync)(hookPath, 493);
8808
- console.log("Quikcommit hook installed.");
8809
- console.log("Now just run `git commit` and a message will be generated automatically.");
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 import_fs8, import_path8, import_child_process6, HOOK_CONTENT;
8812
- var init_init = __esm({
8813
- "src/commands/init.ts"() {
9072
+ var init_branch_rescue = __esm({
9073
+ "src/branch-rescue.ts"() {
8814
9074
  "use strict";
8815
- import_fs8 = require("fs");
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/commands/team.ts
8850
- var team_exports = {};
8851
- __export(team_exports, {
8852
- team: () => team
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
- function createApiClient() {
8855
- return new ApiClient();
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 mapCommitlintToRules(config2) {
8858
- if (!config2 || typeof config2 !== "object") return null;
8859
- const c = config2;
8860
- const rules = {};
8861
- const _ext = c.extends;
8862
- const rulesConfig = c.rules;
8863
- if (Array.isArray(rulesConfig?.["type-enum"]) && rulesConfig["type-enum"].length >= 3) {
8864
- const [, , value] = rulesConfig["type-enum"];
8865
- if (Array.isArray(value)) rules.types = value;
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
- if (Array.isArray(rulesConfig?.["scope-enum"]) && rulesConfig["scope-enum"].length >= 3) {
8868
- const [, , value] = rulesConfig["scope-enum"];
8869
- if (Array.isArray(value)) rules.scopes = value;
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
- if (Array.isArray(rulesConfig?.["header-max-length"]) && rulesConfig["header-max-length"].length >= 3) {
8872
- const [, , maxLen] = rulesConfig["header-max-length"];
8873
- if (typeof maxLen === "number") rules.headerMaxLength = maxLen;
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
- if (Array.isArray(rulesConfig?.["subject-case"]) && rulesConfig["subject-case"].length >= 3) {
8876
- const [, , val] = rulesConfig["subject-case"];
8877
- if (val != null) rules.subjectCase = Array.isArray(val) ? val : [val];
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
- return Object.keys(rules).length > 0 ? rules : null;
9180
+ if (options.skipUniqueness) {
9181
+ return candidate;
9182
+ }
9183
+ return ensureUniqueName(candidate, exists);
8880
9184
  }
8881
- function detectLocalCommitlintRules() {
8882
- const cwd = process.cwd();
8883
- const files = [
8884
- ".commitlintrc.json",
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
- const pkgPath = (0, import_path9.join)(cwd, "package.json");
8907
- if ((0, import_fs9.existsSync)(pkgPath)) {
8908
- try {
8909
- const content = (0, import_fs9.readFileSync)(pkgPath, "utf-8");
8910
- const pkg = JSON.parse(content);
8911
- if (pkg.commitlint) {
8912
- const rules = mapCommitlintToRules(pkg.commitlint);
8913
- if (rules) return rules;
8914
- }
8915
- } catch {
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
- const config2 = getConfig();
8919
- if (config2.rules && Object.keys(config2.rules).length > 0) {
8920
- return config2.rules;
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
- async function team(subcommand, args) {
8925
- const api = createApiClient();
8926
- switch (subcommand) {
8927
- case void 0:
8928
- case "info": {
8929
- const info = await api.getTeam();
8930
- console.log(`
8931
- Team: ${info.name}`);
8932
- console.log(` Plan: ${info.plan}`);
8933
- console.log(` Members: ${info.member_count}`);
8934
- console.log("\n Members:");
8935
- for (const m of info.members) {
8936
- console.log(` ${m.name ?? m.email} <${m.email}> (${m.role})`);
8937
- }
8938
- break;
8939
- }
8940
- case "rules": {
8941
- if (args?.[0] === "push") {
8942
- const rules = detectLocalCommitlintRules();
8943
- if (!rules) {
8944
- console.error("No local commitlint config found.");
8945
- process.exit(1);
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
- await api.inviteTeamMember(email);
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
- var import_fs9, import_path9;
8973
- var init_team = __esm({
8974
- "src/commands/team.ts"() {
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
- import_fs9 = require("fs");
8977
- import_path9 = require("path");
8978
- init_api();
8979
- init_config();
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/commands/config.ts
8984
- var config_exports = {};
8985
- __export(config_exports, {
8986
- config: () => config
8987
- });
8988
- function config(args) {
8989
- if (args.length === 0) {
8990
- showConfig();
8991
- return;
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
- const sub = args[0];
8994
- if (sub === "set") {
8995
- const key = args[1];
8996
- const value = args[2];
8997
- if (!key || !value) {
8998
- console.error("Usage: qc config set <key> <value>");
8999
- console.error(" Keys: model, api_url, provider, auto_stage");
9000
- process.exit(1);
9001
- }
9002
- setConfig(key, value);
9003
- return;
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
- if (sub === "reset") {
9006
- resetConfig();
9007
- return;
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
- console.error(`Unknown subcommand: ${sub}`);
9010
- console.error("Usage: qc config [set <key> <value> | reset]");
9011
- process.exit(1);
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 showConfig() {
9014
- const cfg = getConfig();
9015
- const apiKey = getApiKey();
9016
- console.log("Current configuration:");
9017
- console.log(` model: ${cfg.model ?? "(default for plan)"}`);
9018
- console.log(` api_url: ${cfg.apiUrl ?? DEFAULT_API_URL}`);
9019
- console.log(` provider: ${cfg.provider ?? "(default)"}`);
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 resetConfig() {
9057
- saveConfig({});
9058
- console.log("Config reset to defaults.");
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 init_config2 = __esm({
9061
- "src/commands/config.ts"() {
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
- init_config();
9064
- init_dist();
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/commands/upgrade.ts
9069
- var upgrade_exports = {};
9070
- __export(upgrade_exports, {
9071
- upgrade: () => upgrade
9072
- });
9073
- async function upgrade() {
9074
- console.log(`
9075
- Opening ${BILLING_URL}
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 { execFileSync: execFileSync7 } = await import("child_process");
9079
- if (process.platform === "darwin") {
9080
- execFileSync7("open", [BILLING_URL]);
9081
- } else if (process.platform === "linux") {
9082
- execFileSync7("xdg-open", [BILLING_URL]);
9083
- } else if (process.platform === "win32") {
9084
- execFileSync7("cmd", ["/c", "start", "", BILLING_URL]);
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
- } catch {
9087
- console.log(`Visit: ${BILLING_URL}`);
9550
+ return { action: "commit" };
9551
+ } finally {
9552
+ rl.close();
9088
9553
  }
9089
9554
  }
9090
- var BILLING_URL;
9091
- var init_upgrade = __esm({
9092
- "src/commands/upgrade.ts"() {
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
- BILLING_URL = "https://app.quikcommit.dev/billing";
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 preprocessDiff(diff) {
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) return { processedDiff: diff, summarized: [], tokensSaved: 0 };
9139
- const kept = [];
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
- kept.push(file.content);
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: kept.join(""),
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
- // ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
9215
- var require_picocolors = __commonJS({
9216
- "../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports2, module2) {
9217
- var p = process || {};
9218
- var argv = p.argv || [];
9219
- var env = p.env || {};
9220
- var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
9221
- var formatter = (open, close, replace = open) => (input) => {
9222
- let string = "" + input, index = string.indexOf(close, open.length);
9223
- return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
9224
- };
9225
- var replaceClose = (string, close, replace, index) => {
9226
- let result = "", cursor = 0;
9227
- do {
9228
- result += string.substring(cursor, index) + replace;
9229
- cursor = index + close.length;
9230
- index = string.indexOf(close, cursor);
9231
- } while (~index);
9232
- return result + string.substring(cursor);
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
- return process.argv.slice(2).includes("--no-color");
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 createUI(options) {
9295
- const isColor = options.isTTY && !options.noColor;
9296
- const wrap = (fn) => (s) => isColor ? fn(s) : s;
9297
- const format = {
9298
- step: (msg) => `${isColor ? import_picocolors.default.dim("\u203A") : "\u203A"} ${isColor ? import_picocolors.default.dim(msg) : msg}`,
9299
- success: (msg) => `${isColor ? import_picocolors.default.green("\u2713") : "\u2713"} ${msg}`,
9300
- error: (msg) => `${isColor ? import_picocolors.default.red("\u2717") : "\u2717"} ${msg}`,
9301
- dim: wrap(import_picocolors.default.dim),
9302
- bold: wrap(import_picocolors.default.bold),
9303
- commitType: wrap(import_picocolors.default.cyan),
9304
- commitScope: wrap(import_picocolors.default.yellow)
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
- function createSpinner(message, write = (s) => process.stderr.write(s)) {
9307
- let frame = 0;
9308
- let interval = null;
9309
- return {
9310
- start() {
9311
- if (interval) return;
9312
- if (!options.isTTY) return;
9313
- interval = setInterval(() => {
9314
- const f = SPINNER_FRAMES2[frame++ % SPINNER_FRAMES2.length];
9315
- write(`\r${format.step(message)} ${isColor ? import_picocolors.default.cyan(f) : f}`);
9316
- }, 80);
9317
- },
9318
- stop(finalMessage) {
9319
- if (interval) {
9320
- clearInterval(interval);
9321
- interval = null;
9322
- }
9323
- if (options.isTTY) {
9324
- write("\r\x1B[2K");
9325
- }
9326
- if (finalMessage) {
9327
- write(finalMessage + "\n");
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 getUI() {
9341
- if (!_defaultUI) {
9342
- _defaultUI = createUI({
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
- noColor: !!process.env.NO_COLOR || hasCliNoColor()
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
- return _defaultUI;
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
- var import_picocolors, SPINNER_FRAMES2, _defaultUI, ui;
9350
- var init_ui = __esm({
9351
- "src/ui.ts"() {
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
- import_picocolors = __toESM(require_picocolors());
9354
- SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
9355
- ui = new Proxy({}, {
9356
- get(_target, prop) {
9357
- return getUI()[prop];
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
- // src/commit-helpers.ts
9364
- function applyCliTypeScopeToRules(rules, type, scope) {
9365
- let next = { ...rules };
9366
- if (type) {
9367
- next = { ...next, types: [type] };
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
- if (scope) {
9370
- next = { ...next, scopes: [scope] };
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
- return next;
9373
- }
9374
- function generationHintsFromArgs(split, forceBody) {
9375
- const h = {};
9376
- if (split) h.split = true;
9377
- if (forceBody) h.force_body = true;
9378
- return Object.keys(h).length > 0 ? h : void 0;
9379
- }
9380
- function splitCommitMessageForDisplay(message) {
9381
- const t = message.replace(/\r\n/g, "\n").trimEnd();
9382
- const doubleNl = t.indexOf("\n\n");
9383
- if (doubleNl !== -1) {
9384
- const head = t.slice(0, doubleNl);
9385
- const subject = head.split("\n")[0]?.trim() ?? "";
9386
- return { subject, body: t.slice(doubleNl + 2).trimEnd() };
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 firstNl = t.indexOf("\n");
9389
- if (firstNl === -1) {
9390
- return { subject: t.trim(), body: "" };
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
- return {
9393
- subject: t.slice(0, firstNl).trim(),
9394
- body: t.slice(firstNl + 1).trimEnd()
9395
- };
9396
- }
9397
- function formatVerboseCommitDiagnostics(diagnostics, roundTripMs) {
9398
- const lines = [`api_round_trip_ms: ${roundTripMs}`];
9399
- if (diagnostics !== void 0) {
9400
- lines.push(JSON.stringify(diagnostics, null, 2));
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
- async function interactiveRefineMessage(initial, opts) {
9405
- if (opts.skip) return { action: "accept", message: initial };
9406
- const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
9407
- try {
9408
- process.stderr.write(`
9409
- ${initial}
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
- const choice = (await rl.question("Keep? [Y/n/e]: ")).trim().toLowerCase();
9413
- if (choice === "n") {
9414
- return { action: "abort" };
9415
- }
9416
- if (choice === "e") {
9417
- process.stderr.write("Enter new message (end with a line containing only .):\n");
9418
- const lines = [];
9419
- while (true) {
9420
- const line = await rl.question("");
9421
- if (line === ".") break;
9422
- lines.push(line);
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
- const edited = lines.join("\n").trim();
9425
- return { action: "edit", message: edited.length > 0 ? edited : initial };
10591
+ } else {
10592
+ console.log("No hook to remove.");
9426
10593
  }
9427
- return { action: "accept", message: initial };
9428
- } finally {
9429
- rl.close();
10594
+ return;
9430
10595
  }
9431
- }
9432
- async function confirmCommit(prompt2, opts) {
9433
- if (opts.skip) return { action: "commit" };
9434
- const rl = import_promises.default.createInterface({ input: process.stdin, output: process.stderr });
9435
- try {
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
- return { action: "commit" };
9441
- } finally {
9442
- rl.close();
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
- function shouldSkipTTYInteraction(hookMode) {
9446
- return hookMode === true || process.stdin.isTTY !== true;
9447
- }
9448
- function logVerboseDiagnostics(dim, verbose, quiet, diagnostics, roundTripMs) {
9449
- if (!verbose || quiet) return;
9450
- process.stderr.write(
9451
- `
9452
- ${formatVerboseCommitDiagnostics(diagnostics, roundTripMs)}
9453
- `
9454
- );
9455
- dim("(verbose diagnostics on stderr)");
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 createSilentLog() {
9458
- return {
9459
- step: () => {
9460
- },
9461
- success: () => {
9462
- },
9463
- error: (msg) => console.error(msg),
9464
- dim: () => {
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 displayCommitMessage(message, log) {
9469
- const { subject, body } = splitCommitMessageForDisplay(message);
9470
- log.success(subject);
9471
- if (body) {
9472
- for (const line of body.split("\n")) {
9473
- log.dim(` ${line}`);
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
- // src/local.ts
9487
- var local_exports = {};
9488
- __export(local_exports, {
9489
- getLocalProviderConfig: () => getLocalProviderConfig,
9490
- runLocalCommit: () => runLocalCommit
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
- } catch {
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 getLegacyBaseUrl(provider) {
9506
- try {
9507
- const p = (0, import_path10.join)(CONFIG_PATH2, "base_url");
9508
- if ((0, import_fs10.existsSync)(p)) {
9509
- return (0, import_fs10.readFileSync)(p, "utf-8").trim();
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
- } catch {
9512
- }
9513
- return PROVIDER_URLS[provider] ?? "";
9514
- }
9515
- function getLegacyModel(provider) {
9516
- try {
9517
- const p = (0, import_path10.join)(CONFIG_PATH2, "model");
9518
- if ((0, import_fs10.existsSync)(p)) {
9519
- const v = (0, import_fs10.readFileSync)(p, "utf-8").trim();
9520
- if (v) return v;
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
- } catch {
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
- function buildUserPrompt(changes, diff, rules, recentCommits, hints) {
9538
- let prompt2 = `Generate a commit message for these changes:
9539
-
9540
- ## File changes:
9541
- <file_changes>
9542
- ${changes}
9543
- </file_changes>
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
- if (hints?.split) {
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
- if (hints?.force_body) {
9564
- prompt2 += `The user requires a BODY section after the subject line, even for small changes.
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 (rules && Object.keys(rules).length > 0) {
9569
- prompt2 += `Rules: ${JSON.stringify(rules)}
9570
-
9571
- `;
10806
+ if (sub === "reset") {
10807
+ resetConfig();
10808
+ return;
9572
10809
  }
9573
- prompt2 += `Important:
9574
- - Follow conventional commit format: <type>(<scope>): <subject>
9575
- - Response should be the commit message only, no explanations`;
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 buildRequest(provider, baseUrl, userContent, diff, changes, model, apiKey, rules, recentCommits, hints) {
9579
- const headers = {
9580
- "Content-Type": "application/json"
9581
- };
9582
- if (apiKey) {
9583
- headers["Authorization"] = `Bearer ${apiKey}`;
9584
- }
9585
- if (provider === "openrouter") {
9586
- headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
9587
- headers["X-Title"] = "qc - AI Commit Message Generator";
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
- let url;
9590
- let body;
9591
- switch (provider) {
9592
- case "ollama":
9593
- url = `${baseUrl}/api/generate`;
9594
- body = {
9595
- model,
9596
- prompt: userContent,
9597
- stream: false,
9598
- options: {}
9599
- };
9600
- return { url, body, headers: { "Content-Type": "application/json" } };
9601
- case "lmstudio":
9602
- url = `${baseUrl}/chat/completions`;
9603
- body = {
9604
- model,
9605
- stream: false,
9606
- messages: [
9607
- {
9608
- role: "system",
9609
- content: "You are a git commit message generator. Create conventional commit messages."
9610
- },
9611
- { role: "user", content: userContent }
9612
- ]
9613
- };
9614
- return { url, body, headers: { "Content-Type": "application/json" } };
9615
- case "openrouter":
9616
- case "custom":
9617
- url = `${baseUrl}/chat/completions`;
9618
- body = {
9619
- model,
9620
- stream: false,
9621
- messages: [
9622
- {
9623
- role: "system",
9624
- content: "You are a git commit message generator. Create conventional commit messages."
9625
- },
9626
- { role: "user", content: userContent }
9627
- ]
9628
- };
9629
- return { url, body, headers };
9630
- case "cloudflare": {
9631
- url = `${baseUrl.replace(/\/$/, "")}/commit`;
9632
- const payload = { diff, changes, rules };
9633
- if (recentCommits && recentCommits.length > 0) {
9634
- payload.recent_commits = recentCommits.slice(0, 10);
9635
- }
9636
- if (hints && Object.keys(hints).length > 0) {
9637
- payload.generation_hints = hints;
9638
- }
9639
- body = payload;
9640
- return { url, body, headers: { "Content-Type": "application/json" } };
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
- default:
9643
- throw new Error(`Unknown provider: ${provider}`);
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 parseResponse(provider, data) {
9647
- const r = data;
9648
- switch (provider) {
9649
- case "ollama":
9650
- return r.response ?? "";
9651
- case "lmstudio":
9652
- case "openrouter":
9653
- case "custom": {
9654
- const choices = r.choices;
9655
- return choices?.[0]?.message?.content ?? "";
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
- case "cloudflare":
9658
- return r.commit?.response ?? "";
9659
- default:
9660
- return "";
10887
+ } catch {
10888
+ console.log(`Visit: ${BILLING_URL}`);
9661
10889
  }
9662
10890
  }
9663
- async function runLocalCommit(args) {
9664
- const silent = !!(args.hookMode || args.quiet);
9665
- const log = silent ? createSilentLog() : getUI().log;
9666
- if (!isGitRepo()) {
9667
- throw new Error("Not a git repository.");
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
- const local = getLocalProviderConfig();
9673
- if (!local) {
9674
- throw new Error(
9675
- "No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
9676
- );
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
- const config2 = getConfig();
9679
- const excludes = [...config2.excludes ?? [], ...args.exclude];
9680
- let diff = getStagedDiff(excludes);
9681
- const changes = getStagedFiles();
9682
- if (!args.noSmartDiff) {
9683
- const smartResult = preprocessDiff(diff);
9684
- diff = smartResult.processedDiff;
9685
- if (smartResult.summarized.length > 0 && !silent) {
9686
- log.step(
9687
- `smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
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
- let rules = { ...await detectCommitlintRules(), ...config2.rules ?? {} };
9692
- const workspace = detectWorkspace();
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
- rules = applyCliTypeScopeToRules(rules, args.type, args.scope);
9702
- const recentCommits = args.noContext ? void 0 : getRecentBranchCommits(5);
9703
- const generationHints = generationHintsFromArgs(args.split, args.forceBody);
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
- const spinner = getUI().spinner(`generating commit (${modelDisplay} via ${local.provider})...`);
9733
- if (!silent) spinner.start();
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
- const roundTripMs = Date.now() - t0;
9746
- if (!res.ok) {
9747
- const text = await res.text();
9748
- throw new Error(`Provider error (${res.status}): ${text}`);
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 data = await res.json();
9751
- let message = parseResponse(local.provider, data);
9752
- message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
9753
- if (!message) {
9754
- throw new Error("Failed to generate commit message.");
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 diagnostics = local.provider === "cloudflare" && typeof data === "object" && data !== null ? data.diagnostics : void 0;
9757
- logVerboseDiagnostics((msg) => log.dim(msg), args.verbose, args.quiet, diagnostics, roundTripMs);
9758
- if (args.interactive) {
9759
- if (shouldSkipTTYInteraction(args.hookMode)) {
9760
- if (!silent) log.dim("(--interactive ignored: not running in a TTY)");
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
- const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
9763
- if (refineResult.action === "abort") {
9764
- process.exit(0);
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 (args.messageOnly) {
9770
- console.log(message);
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
- if (args.confirm) {
9780
- const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
9781
- if (confirmResult.action === "abort") {
9782
- process.exit(0);
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
- gitCommit(message);
9786
- if (args.push) {
9787
- gitPush();
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 import_fs10, import_path10, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
9791
- var init_local = __esm({
9792
- "src/local.ts"() {
11066
+ var import_promises2;
11067
+ var init_branch_guard = __esm({
11068
+ "src/branch-guard.ts"() {
9793
11069
  "use strict";
9794
- import_fs10 = require("fs");
9795
- import_path10 = require("path");
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
- init_monorepo();
9801
- init_commitlint();
9802
- init_smart_diff();
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 log = silent ? createSilentLog() : getUI().log;
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 { files, total } = getShortStagedFiles();
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 tracked files, or `git add` manually.");
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 = preprocessDiff(diff);
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 = getUI().spinner(`generating commit (${modelDisplay})...`);
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
- process.exit(0);
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
- displayCommitMessage(message, log);
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
- process.exit(0);
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
- const stats = getPushStats();
9957
- if (stats) {
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(), config_exports));
11680
+ const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
10340
11681
  config2(values.positionals);
10341
11682
  return;
10342
11683
  }