@quikcommit/cli 8.0.0 → 9.0.0

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