@quikcommit/cli 7.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 +3004 -923
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
33
  mod
34
34
  ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
36
 
36
37
  // ../shared/dist/types.js
37
38
  var init_types = __esm({
@@ -61,6 +62,80 @@ var init_rules = __esm({
61
62
  }
62
63
  });
63
64
 
65
+ // ../shared/dist/tokens.js
66
+ function estimateTokens(text) {
67
+ return Math.ceil(text.length / 2.5);
68
+ }
69
+ var init_tokens = __esm({
70
+ "../shared/dist/tokens.js"() {
71
+ "use strict";
72
+ }
73
+ });
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
+
64
139
  // ../shared/dist/index.js
65
140
  var init_dist = __esm({
66
141
  "../shared/dist/index.js"() {
@@ -68,10 +143,20 @@ var init_dist = __esm({
68
143
  init_types();
69
144
  init_constants();
70
145
  init_rules();
146
+ init_tokens();
147
+ init_branch();
71
148
  }
72
149
  });
73
150
 
74
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
+ });
75
160
  function getApiKey() {
76
161
  const envKey = process.env.QC_API_KEY;
77
162
  if (envKey?.trim()) return envKey.trim();
@@ -125,6 +210,149 @@ var init_config = __esm({
125
210
  }
126
211
  });
127
212
 
213
+ // src/commands/login.ts
214
+ var login_exports = {};
215
+ __export(login_exports, {
216
+ runLogin: () => runLogin
217
+ });
218
+ function openBrowser(url) {
219
+ try {
220
+ if ((0, import_os2.platform)() === "darwin") {
221
+ (0, import_child_process.execFileSync)("open", [url], { stdio: "pipe" });
222
+ return true;
223
+ }
224
+ if ((0, import_os2.platform)() === "linux") {
225
+ (0, import_child_process.execFileSync)("xdg-open", [url], { stdio: "pipe" });
226
+ return true;
227
+ }
228
+ if ((0, import_os2.platform)() === "win32") {
229
+ (0, import_child_process.execFileSync)("cmd", ["/c", "start", "", url], { stdio: "pipe" });
230
+ return true;
231
+ }
232
+ } catch {
233
+ }
234
+ return false;
235
+ }
236
+ async function runLogin() {
237
+ const codeRes = await fetch(`${API_URL}/api/auth/device/code`, {
238
+ method: "POST",
239
+ headers: { "Content-Type": "application/json" },
240
+ body: JSON.stringify({ client_id: CLIENT_ID })
241
+ });
242
+ if (!codeRes.ok) {
243
+ const err = await codeRes.json().catch(() => ({ error: codeRes.statusText }));
244
+ throw new Error(err.error ?? "Failed to start device flow");
245
+ }
246
+ const codeData = await codeRes.json();
247
+ const { device_code, user_code, verification_uri_complete, interval = 5 } = codeData;
248
+ if (!device_code || !user_code) {
249
+ throw new Error("Server did not return device codes");
250
+ }
251
+ console.log("Opening browser to sign in...");
252
+ console.log("");
253
+ console.log(` Your code: ${user_code}`);
254
+ console.log("");
255
+ const authUrl = verification_uri_complete ?? `${DASHBOARD_URL}/device?user_code=${encodeURIComponent(user_code)}`;
256
+ const opened = openBrowser(authUrl);
257
+ if (!opened) {
258
+ console.log("Could not open browser. Please visit:");
259
+ console.log(authUrl);
260
+ console.log("");
261
+ }
262
+ let frame = 0;
263
+ const spinner = setInterval(() => {
264
+ const elapsed = Math.floor((Date.now() - startTime) / 1e3);
265
+ process.stderr.write(
266
+ `\r${SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]} Waiting for authorization... (${elapsed}s)`
267
+ );
268
+ }, 80);
269
+ let pollingInterval = interval * 1e3;
270
+ const startTime = Date.now();
271
+ try {
272
+ while (Date.now() - startTime < DEVICE_FLOW_TIMEOUT) {
273
+ await new Promise((r) => setTimeout(r, pollingInterval));
274
+ try {
275
+ const tokenRes = await fetch(`${API_URL}/api/auth/device/token`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({
279
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
280
+ device_code,
281
+ client_id: CLIENT_ID
282
+ })
283
+ });
284
+ const tokenData = await tokenRes.json();
285
+ if (tokenData.access_token) {
286
+ saveApiKey(tokenData.access_token);
287
+ process.stderr.write("\r\x1B[2K");
288
+ console.log("Successfully logged in!");
289
+ return;
290
+ }
291
+ if (tokenData.error) {
292
+ switch (tokenData.error) {
293
+ case "authorization_pending":
294
+ break;
295
+ // continue polling
296
+ case "slow_down":
297
+ pollingInterval += 5e3;
298
+ break;
299
+ case "access_denied":
300
+ process.stderr.write("\r\x1B[2K");
301
+ console.error("Authorization was denied.");
302
+ process.exit(1);
303
+ break;
304
+ case "expired_token":
305
+ process.stderr.write("\r\x1B[2K");
306
+ console.error("Device code expired. Please try again.");
307
+ process.exit(1);
308
+ break;
309
+ default:
310
+ process.stderr.write("\r\x1B[2K");
311
+ console.error(`Error: ${tokenData.error_description ?? tokenData.error}`);
312
+ process.exit(1);
313
+ }
314
+ }
315
+ } catch {
316
+ }
317
+ }
318
+ process.stderr.write("\r\x1B[2K");
319
+ console.error("Login timed out. Please try again.");
320
+ process.exit(1);
321
+ } finally {
322
+ clearInterval(spinner);
323
+ }
324
+ }
325
+ var import_child_process, import_os2, API_URL, DASHBOARD_URL, CLIENT_ID, SPINNER_FRAMES;
326
+ var init_login = __esm({
327
+ "src/commands/login.ts"() {
328
+ "use strict";
329
+ import_child_process = require("child_process");
330
+ import_os2 = require("os");
331
+ init_config();
332
+ init_dist();
333
+ API_URL = process.env.QC_API_URL ?? DEFAULT_API_URL;
334
+ DASHBOARD_URL = "https://app.quikcommit.dev";
335
+ CLIENT_ID = "qc-cli";
336
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
337
+ }
338
+ });
339
+
340
+ // src/commands/logout.ts
341
+ var logout_exports = {};
342
+ __export(logout_exports, {
343
+ runLogout: () => runLogout
344
+ });
345
+ function runLogout() {
346
+ clearApiKey();
347
+ console.log("Logged out. Credentials cleared.");
348
+ }
349
+ var init_logout = __esm({
350
+ "src/commands/logout.ts"() {
351
+ "use strict";
352
+ init_config();
353
+ }
354
+ });
355
+
128
356
  // src/api.ts
129
357
  var ApiClient;
130
358
  var init_api = __esm({
@@ -155,6 +383,13 @@ var init_api = __esm({
155
383
  body: JSON.stringify(body)
156
384
  });
157
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
+ }
158
393
  const err = await res.json().catch(() => ({ error: res.statusText }));
159
394
  if (planRequiredMsg && err.code === "PLAN_REQUIRED") {
160
395
  throw new Error(planRequiredMsg);
@@ -163,8 +398,15 @@ var init_api = __esm({
163
398
  }
164
399
  return res.json();
165
400
  }
166
- async generateCommit(diff, changes, rules, model) {
167
- const body = { diff, changes, rules, model };
401
+ async generateCommit(diff, changes, rules, model, recentCommits, generationHints) {
402
+ const body = {
403
+ diff,
404
+ changes,
405
+ rules,
406
+ model,
407
+ recent_commits: recentCommits,
408
+ ...generationHints && Object.keys(generationHints).length > 0 ? { generation_hints: generationHints } : {}
409
+ };
168
410
  const data = await this.request(
169
411
  "/v1/commit",
170
412
  body
@@ -194,6 +436,9 @@ var init_api = __esm({
194
436
  summary: data.summary ?? ""
195
437
  };
196
438
  }
439
+ async generateBranchName(req) {
440
+ return this.request("/v1/branch", req);
441
+ }
197
442
  async fetchJson(endpoint, options) {
198
443
  if (!this.apiKey) {
199
444
  throw new Error("Not authenticated. Run `qc login` first.");
@@ -248,6 +493,37 @@ var init_api = __esm({
248
493
  }
249
494
  });
250
495
 
496
+ // src/commands/status.ts
497
+ var status_exports = {};
498
+ __export(status_exports, {
499
+ runStatus: () => runStatus
500
+ });
501
+ async function runStatus(apiKeyFlag) {
502
+ const apiKey = apiKeyFlag ?? getApiKey();
503
+ if (!apiKey) {
504
+ console.log("Not logged in. Run `qc login` to authenticate.");
505
+ return;
506
+ }
507
+ console.log("Logged in: yes");
508
+ console.log(` API key: ...${apiKey.slice(-4)}`);
509
+ const client = new ApiClient({ apiKey });
510
+ const usage = await client.getUsage();
511
+ if (usage) {
512
+ console.log(`Plan: ${usage.plan}`);
513
+ console.log(`Usage: ${usage.commit_count}/${usage.limit} commits this period`);
514
+ console.log(`Remaining: ${usage.remaining}`);
515
+ } else {
516
+ console.log("Usage: (unable to fetch)");
517
+ }
518
+ }
519
+ var init_status = __esm({
520
+ "src/commands/status.ts"() {
521
+ "use strict";
522
+ init_config();
523
+ init_api();
524
+ }
525
+ });
526
+
251
527
  // ../../node_modules/.pnpm/yaml@2.8.4/node_modules/yaml/dist/nodes/identity.js
252
528
  var require_identity = __commonJS({
253
529
  "../../node_modules/.pnpm/yaml@2.8.4/node_modules/yaml/dist/nodes/identity.js"(exports2) {
@@ -7558,13 +7834,16 @@ var require_dist = __commonJS({
7558
7834
 
7559
7835
  // src/git.ts
7560
7836
  function validateRef(ref, name = "ref") {
7837
+ if (ref.startsWith("-")) {
7838
+ throw new Error(`Invalid git ref ${name}: starts with hyphen: "${ref}"`);
7839
+ }
7561
7840
  if (!ref || !SAFE_GIT_REF.test(ref)) {
7562
7841
  throw new Error(`Invalid git ref ${name}: "${ref}"`);
7563
7842
  }
7564
7843
  }
7565
7844
  function isGitRepo() {
7566
7845
  try {
7567
- (0, import_child_process.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], {
7846
+ (0, import_child_process2.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], {
7568
7847
  stdio: "pipe"
7569
7848
  });
7570
7849
  return true;
@@ -7574,7 +7853,7 @@ function isGitRepo() {
7574
7853
  }
7575
7854
  function getGitRoot() {
7576
7855
  try {
7577
- return (0, import_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
7856
+ return (0, import_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
7578
7857
  encoding: "utf-8"
7579
7858
  }).trim();
7580
7859
  } catch {
@@ -7590,37 +7869,37 @@ function getStagedDiff(excludes = []) {
7590
7869
  args.push(`:(exclude)${pattern}`);
7591
7870
  }
7592
7871
  }
7593
- return (0, import_child_process.execFileSync)("git", args, {
7872
+ return (0, import_child_process2.execFileSync)("git", args, {
7594
7873
  encoding: "utf-8",
7595
7874
  maxBuffer: 10 * 1024 * 1024
7596
7875
  });
7597
7876
  }
7598
7877
  function getStagedFiles() {
7599
- return (0, import_child_process.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7878
+ return (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7600
7879
  encoding: "utf-8"
7601
7880
  });
7602
7881
  }
7603
7882
  function hasStagedChanges() {
7604
- const output = (0, import_child_process.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7883
+ const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7605
7884
  encoding: "utf-8"
7606
7885
  });
7607
7886
  return output.trim().length > 0;
7608
7887
  }
7609
7888
  function getUnstagedFiles() {
7610
- const output = (0, import_child_process.execFileSync)("git", ["status", "--porcelain"], {
7889
+ const output = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], {
7611
7890
  encoding: "utf-8"
7612
7891
  });
7613
7892
  return output.trim().split("\n").filter(Boolean).filter((line) => !line.startsWith("??"));
7614
7893
  }
7615
7894
  function stageAll() {
7616
- (0, import_child_process.execFileSync)("git", ["add", "-u"], { stdio: "pipe" });
7895
+ (0, import_child_process2.execFileSync)("git", ["add", "-u"], { stdio: "pipe" });
7617
7896
  }
7618
7897
  function gitCommit(message) {
7619
- const tmpDir = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os2.tmpdir)(), "qc-"));
7898
+ const tmpDir = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os3.tmpdir)(), "qc-"));
7620
7899
  const tmpFile = (0, import_path2.join)(tmpDir, "commit.txt");
7621
7900
  (0, import_fs2.writeFileSync)(tmpFile, message, { mode: 384 });
7622
7901
  try {
7623
- (0, import_child_process.execFileSync)("git", ["commit", "-F", tmpFile], { stdio: "inherit" });
7902
+ (0, import_child_process2.execFileSync)("git", ["commit", "-F", tmpFile], { stdio: "inherit" });
7624
7903
  } finally {
7625
7904
  try {
7626
7905
  (0, import_fs2.unlinkSync)(tmpFile);
@@ -7630,11 +7909,11 @@ function gitCommit(message) {
7630
7909
  }
7631
7910
  }
7632
7911
  function gitPush() {
7633
- (0, import_child_process.execFileSync)("git", ["push"], { stdio: "inherit" });
7912
+ (0, import_child_process2.execFileSync)("git", ["push"], { stdio: "inherit" });
7634
7913
  }
7635
7914
  function getBranchCommits(base = "main") {
7636
7915
  validateRef(base, "base");
7637
- const output = (0, import_child_process.execFileSync)("git", ["log", `${base}..HEAD`, "--format=%s", "--max-count=1000"], {
7916
+ const output = (0, import_child_process2.execFileSync)("git", ["log", `${base}..HEAD`, "--format=%s", "--max-count=1000"], {
7638
7917
  encoding: "utf-8",
7639
7918
  maxBuffer: 10 * 1024 * 1024
7640
7919
  });
@@ -7642,19 +7921,19 @@ function getBranchCommits(base = "main") {
7642
7921
  }
7643
7922
  function getDiffStat(base = "main") {
7644
7923
  validateRef(base, "base");
7645
- return (0, import_child_process.execFileSync)("git", ["diff", `${base}..HEAD`, "--stat"], {
7924
+ return (0, import_child_process2.execFileSync)("git", ["diff", `${base}..HEAD`, "--stat"], {
7646
7925
  encoding: "utf-8",
7647
7926
  maxBuffer: 10 * 1024 * 1024
7648
7927
  });
7649
7928
  }
7650
7929
  function getCurrentBranch() {
7651
- return (0, import_child_process.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
7930
+ return (0, import_child_process2.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
7652
7931
  encoding: "utf-8"
7653
7932
  }).trim();
7654
7933
  }
7655
7934
  function getLatestTag() {
7656
7935
  try {
7657
- return (0, import_child_process.execFileSync)("git", ["describe", "--tags", "--abbrev=0"], {
7936
+ return (0, import_child_process2.execFileSync)("git", ["describe", "--tags", "--abbrev=0"], {
7658
7937
  encoding: "utf-8"
7659
7938
  }).trim();
7660
7939
  } catch {
@@ -7664,7 +7943,7 @@ function getLatestTag() {
7664
7943
  function getCommitsSince(ref, to = "HEAD") {
7665
7944
  validateRef(ref, "from ref");
7666
7945
  validateRef(to, "to ref");
7667
- const output = (0, import_child_process.execFileSync)(
7946
+ const output = (0, import_child_process2.execFileSync)(
7668
7947
  "git",
7669
7948
  ["log", `${ref}..${to}`, "--format=%H %s", "--max-count=1000"],
7670
7949
  { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
@@ -7676,7 +7955,7 @@ function getCommitsSince(ref, to = "HEAD") {
7676
7955
  }
7677
7956
  function getChangedFilesSince(base = "main") {
7678
7957
  validateRef(base, "base");
7679
- const output = (0, import_child_process.execFileSync)("git", ["diff", `${base}..HEAD`, "--name-only"], {
7958
+ const output = (0, import_child_process2.execFileSync)("git", ["diff", `${base}..HEAD`, "--name-only"], {
7680
7959
  encoding: "utf-8",
7681
7960
  maxBuffer: 10 * 1024 * 1024
7682
7961
  });
@@ -7684,7 +7963,7 @@ function getChangedFilesSince(base = "main") {
7684
7963
  }
7685
7964
  function getOnlineLog(base = "main") {
7686
7965
  validateRef(base, "base");
7687
- return (0, import_child_process.execFileSync)(
7966
+ return (0, import_child_process2.execFileSync)(
7688
7967
  "git",
7689
7968
  ["log", `${base}..HEAD`, "--oneline", "--max-count=200"],
7690
7969
  {
@@ -7695,55 +7974,217 @@ function getOnlineLog(base = "main") {
7695
7974
  }
7696
7975
  function getFullDiff(base = "main") {
7697
7976
  validateRef(base, "base");
7698
- return (0, import_child_process.execFileSync)("git", ["diff", `${base}..HEAD`], {
7977
+ return (0, import_child_process2.execFileSync)("git", ["diff", `${base}..HEAD`], {
7699
7978
  encoding: "utf-8",
7700
7979
  maxBuffer: 10 * 1024 * 1024
7701
7980
  });
7702
7981
  }
7703
- var import_child_process, import_fs2, import_path2, import_os2, SAFE_GIT_REF;
7704
- var init_git = __esm({
7705
- "src/git.ts"() {
7706
- "use strict";
7707
- import_child_process = require("child_process");
7708
- import_fs2 = require("fs");
7709
- import_path2 = require("path");
7710
- import_os2 = require("os");
7711
- SAFE_GIT_REF = /^[a-zA-Z0-9._\-/~:^@]+$/;
7712
- }
7713
- });
7714
-
7715
- // src/commitlint.ts
7716
- function findConfigFile(root) {
7717
- for (const file of CONFIG_FILES) {
7718
- const full = (0, import_path3.join)(root, file);
7719
- if ((0, import_fs3.existsSync)(full)) return full;
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 };
7720
7997
  }
7721
- return null;
7722
7998
  }
7723
- function getRule(rules, name) {
7724
- const r = rules[name];
7725
- if (!Array.isArray(r) || r.length < 3) return null;
7726
- const [severity, applicability, value] = r;
7727
- if (typeof severity !== "number" || severity < 1) return null;
7728
- return [severity, applicability, value];
7999
+ function getShortStagedFiles(max = 3) {
8000
+ const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
8001
+ encoding: "utf-8"
8002
+ });
8003
+ const all = output.trim().split("\n").filter(Boolean);
8004
+ return { files: all.slice(0, max), total: all.length };
7729
8005
  }
7730
- function mapRules(rules) {
7731
- const result = {};
7732
- const typeEnum = getRule(rules, "type-enum");
7733
- if (typeEnum && typeEnum[1] === "always" && Array.isArray(typeEnum[2])) {
7734
- result.types = typeEnum[2].filter((t) => typeof t === "string");
7735
- }
7736
- const scopeEnum = getRule(rules, "scope-enum");
7737
- if (scopeEnum && scopeEnum[1] === "always" && Array.isArray(scopeEnum[2])) {
7738
- result.scopes = scopeEnum[2].filter((s) => typeof s === "string");
8006
+ function getCommitHash() {
8007
+ return (0, import_child_process2.execFileSync)("git", ["rev-parse", "--short", "HEAD"], {
8008
+ encoding: "utf-8"
8009
+ }).trim();
8010
+ }
8011
+ function getPushStats() {
8012
+ try {
8013
+ const branch = (0, import_child_process2.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
8014
+ encoding: "utf-8"
8015
+ }).trim();
8016
+ const countOutput = (0, import_child_process2.execFileSync)(
8017
+ "git",
8018
+ ["rev-list", "--count", `origin/${branch}..HEAD`],
8019
+ { encoding: "utf-8" }
8020
+ ).trim();
8021
+ const parsedCount = parseInt(countOutput, 10);
8022
+ const commits = Number.isFinite(parsedCount) ? parsedCount : 0;
8023
+ const stat = (0, import_child_process2.execFileSync)(
8024
+ "git",
8025
+ ["diff", "--shortstat", `origin/${branch}..HEAD`],
8026
+ { encoding: "utf-8" }
8027
+ ).trim();
8028
+ return { commits, stat };
8029
+ } catch {
8030
+ return null;
7739
8031
  }
7740
- const headerMax = getRule(rules, "header-max-length");
7741
- if (headerMax && headerMax[1] === "always" && typeof headerMax[2] === "number") {
7742
- result.headerMaxLength = headerMax[2];
8032
+ }
8033
+ function getRecentBranchCommits(count = 5) {
8034
+ try {
8035
+ const output = (0, import_child_process2.execFileSync)(
8036
+ "git",
8037
+ ["log", "--format=%s%n%b%n---", `--max-count=${count}`, "HEAD"],
8038
+ { encoding: "utf-8", maxBuffer: 1024 * 1024 }
8039
+ );
8040
+ return output.split("---\n").map((entry) => entry.trim()).filter(Boolean).slice(0, count);
8041
+ } catch {
8042
+ return [];
7743
8043
  }
7744
- const subjectMax = getRule(rules, "subject-max-length");
7745
- if (subjectMax && subjectMax[1] === "always" && typeof subjectMax[2] === "number") {
7746
- result.subjectMaxLength = subjectMax[2];
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
+ }
8144
+ var import_child_process2, import_fs2, import_path2, import_os3, SAFE_GIT_REF;
8145
+ var init_git = __esm({
8146
+ "src/git.ts"() {
8147
+ "use strict";
8148
+ import_child_process2 = require("child_process");
8149
+ import_fs2 = require("fs");
8150
+ import_path2 = require("path");
8151
+ import_os3 = require("os");
8152
+ SAFE_GIT_REF = /^[a-zA-Z0-9][a-zA-Z0-9._\-/~:^@]*$/;
8153
+ }
8154
+ });
8155
+
8156
+ // src/commitlint.ts
8157
+ function findConfigFile(root) {
8158
+ for (const file of CONFIG_FILES) {
8159
+ const full = (0, import_path3.join)(root, file);
8160
+ if ((0, import_fs3.existsSync)(full)) return full;
8161
+ }
8162
+ return null;
8163
+ }
8164
+ function getRule(rules, name) {
8165
+ const r = rules[name];
8166
+ if (!Array.isArray(r) || r.length < 3) return null;
8167
+ const [severity, applicability, value] = r;
8168
+ if (typeof severity !== "number" || severity < 1) return null;
8169
+ return [severity, applicability, value];
8170
+ }
8171
+ function mapRules(rules) {
8172
+ const result = {};
8173
+ const typeEnum = getRule(rules, "type-enum");
8174
+ if (typeEnum && typeEnum[1] === "always" && Array.isArray(typeEnum[2])) {
8175
+ result.types = typeEnum[2].filter((t) => typeof t === "string");
8176
+ }
8177
+ const scopeEnum = getRule(rules, "scope-enum");
8178
+ if (scopeEnum && scopeEnum[1] === "always" && Array.isArray(scopeEnum[2])) {
8179
+ result.scopes = scopeEnum[2].filter((s) => typeof s === "string");
8180
+ }
8181
+ const headerMax = getRule(rules, "header-max-length");
8182
+ if (headerMax && headerMax[1] === "always" && typeof headerMax[2] === "number") {
8183
+ result.headerMaxLength = headerMax[2];
8184
+ }
8185
+ const subjectMax = getRule(rules, "subject-max-length");
8186
+ if (subjectMax && subjectMax[1] === "always" && typeof subjectMax[2] === "number") {
8187
+ result.subjectMaxLength = subjectMax[2];
7747
8188
  }
7748
8189
  const bodyMaxLine = getRule(rules, "body-max-line-length");
7749
8190
  if (bodyMaxLine && bodyMaxLine[1] === "always" && typeof bodyMaxLine[2] === "number") {
@@ -7774,7 +8215,7 @@ function mapRulesToCommitRules(rules) {
7774
8215
  }
7775
8216
  function tryNpxPrintConfig(root) {
7776
8217
  try {
7777
- const output = (0, import_child_process2.execFileSync)("npx", ["--no", "commitlint", "--print-config"], {
8218
+ const output = (0, import_child_process3.execFileSync)("npx", ["--no", "commitlint", "--print-config"], {
7778
8219
  encoding: "utf-8",
7779
8220
  cwd: root,
7780
8221
  timeout: 1e4,
@@ -7791,7 +8232,7 @@ function tryNodeEval(configPath) {
7791
8232
  const fileUrl = (0, import_node_url.pathToFileURL)(configPath).href;
7792
8233
  const script = `import cfg from ${JSON.stringify(fileUrl)}; process.stdout.write(JSON.stringify(cfg.default ?? cfg));`;
7793
8234
  try {
7794
- const output = (0, import_child_process2.execFileSync)("node", ["--input-type=module"], {
8235
+ const output = (0, import_child_process3.execFileSync)("node", ["--input-type=module"], {
7795
8236
  input: script,
7796
8237
  encoding: "utf-8",
7797
8238
  timeout: 1e4,
@@ -7808,7 +8249,7 @@ function tryNodeEvalTs(configPath, root) {
7808
8249
  const fileUrl = (0, import_node_url.pathToFileURL)(configPath).href;
7809
8250
  const script = `import cfg from ${JSON.stringify(fileUrl)}; process.stdout.write(JSON.stringify(cfg.default ?? cfg));`;
7810
8251
  try {
7811
- const output = (0, import_child_process2.execFileSync)("node", ["--experimental-strip-types", "--input-type=module"], {
8252
+ const output = (0, import_child_process3.execFileSync)("node", ["--experimental-strip-types", "--input-type=module"], {
7812
8253
  input: script,
7813
8254
  encoding: "utf-8",
7814
8255
  cwd: root,
@@ -7822,7 +8263,7 @@ function tryNodeEvalTs(configPath, root) {
7822
8263
  }
7823
8264
  try {
7824
8265
  const tsxScript = `import cfg from ${JSON.stringify(fileUrl)}; console.log(JSON.stringify(cfg.default ?? cfg));`;
7825
- const output = (0, import_child_process2.execFileSync)("npx", ["--no", "tsx", "-e", tsxScript], {
8266
+ const output = (0, import_child_process3.execFileSync)("npx", ["--no", "tsx", "-e", tsxScript], {
7826
8267
  encoding: "utf-8",
7827
8268
  cwd: root,
7828
8269
  timeout: 15e3,
@@ -7875,11 +8316,11 @@ async function detectCommitlintRules() {
7875
8316
  return void 0;
7876
8317
  }
7877
8318
  }
7878
- var import_child_process2, import_fs3, import_path3, import_node_url, import_yaml, CONFIG_FILES;
8319
+ var import_child_process3, import_fs3, import_path3, import_node_url, import_yaml, CONFIG_FILES;
7879
8320
  var init_commitlint = __esm({
7880
8321
  "src/commitlint.ts"() {
7881
8322
  "use strict";
7882
- import_child_process2 = require("child_process");
8323
+ import_child_process3 = require("child_process");
7883
8324
  import_fs3 = require("fs");
7884
8325
  import_path3 = require("path");
7885
8326
  import_node_url = require("node:url");
@@ -7900,6 +8341,183 @@ var init_commitlint = __esm({
7900
8341
  }
7901
8342
  });
7902
8343
 
8344
+ // src/commands/pr.ts
8345
+ var pr_exports = {};
8346
+ __export(pr_exports, {
8347
+ pr: () => pr
8348
+ });
8349
+ function findPullRequestTemplate(gitRoot) {
8350
+ const fileCandidates = [
8351
+ (0, import_path4.join)(gitRoot, ".github", "pull_request_template.md"),
8352
+ (0, import_path4.join)(gitRoot, ".github", "PULL_REQUEST_TEMPLATE.md"),
8353
+ (0, import_path4.join)(gitRoot, "pull_request_template.md")
8354
+ ];
8355
+ for (const p of fileCandidates) {
8356
+ try {
8357
+ if ((0, import_fs4.existsSync)(p) && (0, import_fs4.statSync)(p).isFile()) {
8358
+ return { path: p, content: (0, import_fs4.readFileSync)(p, "utf-8") };
8359
+ }
8360
+ } catch {
8361
+ }
8362
+ }
8363
+ const multiDir = (0, import_path4.join)(gitRoot, ".github", "PULL_REQUEST_TEMPLATE");
8364
+ try {
8365
+ if ((0, import_fs4.existsSync)(multiDir) && (0, import_fs4.statSync)(multiDir).isDirectory()) {
8366
+ const names = (0, import_fs4.readdirSync)(multiDir).filter((f) => f.toLowerCase().endsWith(".md")).sort();
8367
+ if (names.length > 0) {
8368
+ const p = (0, import_path4.join)(multiDir, names[0]);
8369
+ if ((0, import_fs4.statSync)(p).isFile()) {
8370
+ return { path: p, content: (0, import_fs4.readFileSync)(p, "utf-8") };
8371
+ }
8372
+ }
8373
+ }
8374
+ } catch {
8375
+ }
8376
+ return void 0;
8377
+ }
8378
+ async function pr(options) {
8379
+ const base = options.base ?? "main";
8380
+ const commits = getBranchCommits(base);
8381
+ const diffStat = getDiffStat(base);
8382
+ const gitRoot = getGitRoot();
8383
+ const templateHit = findPullRequestTemplate(gitRoot);
8384
+ let prTemplate;
8385
+ if (templateHit) {
8386
+ prTemplate = templateHit.content.substring(0, 16 * 1024);
8387
+ console.error(`[qc] Using PR template from ${(0, import_path4.relative)(gitRoot, templateHit.path)}`);
8388
+ }
8389
+ const currentBranch = getCurrentBranch().slice(0, MAX_PR_CURRENT_BRANCH_CHARS);
8390
+ if (commits.length === 0) {
8391
+ console.error(`No commits found on this branch vs ${base}`);
8392
+ process.exit(1);
8393
+ }
8394
+ const commitlintRules = await detectCommitlintRules();
8395
+ console.error(`Generating PR description from ${commits.length} commits...`);
8396
+ const apiKey = getApiKey();
8397
+ if (!apiKey) {
8398
+ console.error("Error: Not authenticated. Run `qc login` first.");
8399
+ process.exit(1);
8400
+ }
8401
+ const client = new ApiClient({ apiKey });
8402
+ const result = await client.generatePR(
8403
+ {
8404
+ commits,
8405
+ diff_stat: diffStat,
8406
+ base_branch: base,
8407
+ current_branch: currentBranch,
8408
+ pr_template: prTemplate,
8409
+ rules: commitlintRules
8410
+ },
8411
+ options.model
8412
+ );
8413
+ const trimmedTitle = result.title.trim();
8414
+ if (trimmedTitle) {
8415
+ console.log(`
8416
+ Title: ${trimmedTitle}
8417
+ `);
8418
+ }
8419
+ console.log(result.message + "\n");
8420
+ if (options.create) {
8421
+ try {
8422
+ const prTitle = trimmedTitle || result.message.split("\n").find((l) => l.trim()) || result.message.substring(0, 72).trim();
8423
+ (0, import_child_process4.execFileSync)("gh", ["pr", "create", "--title", prTitle, "--body", result.message], {
8424
+ stdio: "inherit"
8425
+ });
8426
+ } catch {
8427
+ console.error("Error: `gh` CLI not found or failed. Install from https://cli.github.com/");
8428
+ process.exit(1);
8429
+ }
8430
+ }
8431
+ }
8432
+ var import_child_process4, import_fs4, import_path4;
8433
+ var init_pr = __esm({
8434
+ "src/commands/pr.ts"() {
8435
+ "use strict";
8436
+ import_child_process4 = require("child_process");
8437
+ import_fs4 = require("fs");
8438
+ import_path4 = require("path");
8439
+ init_dist();
8440
+ init_config();
8441
+ init_api();
8442
+ init_commitlint();
8443
+ init_git();
8444
+ }
8445
+ });
8446
+
8447
+ // src/commands/changelog.ts
8448
+ var changelog_exports = {};
8449
+ __export(changelog_exports, {
8450
+ changelog: () => changelog
8451
+ });
8452
+ function parseCommitType(subject) {
8453
+ const match = subject.match(CONVENTIONAL_TYPE_RE);
8454
+ return match ? match[1].toLowerCase() : "chore";
8455
+ }
8456
+ function groupCommitsByType(commits) {
8457
+ const byType = {};
8458
+ for (const { subject } of commits) {
8459
+ const type = parseCommitType(subject);
8460
+ if (!byType[type]) byType[type] = [];
8461
+ byType[type].push(subject);
8462
+ }
8463
+ return byType;
8464
+ }
8465
+ async function changelog(options) {
8466
+ const fromRef = options.from ?? getLatestTag();
8467
+ const toRef = options.to ?? "HEAD";
8468
+ if (!fromRef) {
8469
+ console.error("Error: No git tag found. Use --from <ref> to specify a starting point.");
8470
+ process.exit(1);
8471
+ }
8472
+ const commits = getCommitsSince(fromRef, toRef);
8473
+ if (commits.length === 0) {
8474
+ console.error(`No commits found between ${fromRef} and ${toRef}`);
8475
+ process.exit(1);
8476
+ }
8477
+ const commitsByType = groupCommitsByType(commits);
8478
+ const apiKey = getApiKey();
8479
+ if (!apiKey) {
8480
+ console.error("Error: Not authenticated. Run `qc login` first.");
8481
+ process.exit(1);
8482
+ }
8483
+ const client = new ApiClient({ apiKey });
8484
+ const result = await client.generateChangelog(
8485
+ {
8486
+ commits_by_type: commitsByType,
8487
+ from_tag: fromRef,
8488
+ to_ref: toRef
8489
+ },
8490
+ options.model
8491
+ );
8492
+ const version = options.version ?? (/^v?\d/.test(toRef) && toRef !== "HEAD" ? toRef.replace(/^v/, "") : null) ?? `${fromRef.replace(/^v/, "")}-next`;
8493
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8494
+ const header = `## [${version}] - ${date}
8495
+
8496
+ `;
8497
+ const changelogEntry = header + result.message;
8498
+ if (options.write) {
8499
+ const path = (0, import_path5.join)(getGitRoot(), "CHANGELOG.md");
8500
+ const existing = (0, import_fs5.existsSync)(path) ? (0, import_fs5.readFileSync)(path, "utf-8") : "";
8501
+ const newContent = changelogEntry + (existing ? "\n\n" + existing : "");
8502
+ (0, import_fs5.writeFileSync)(path, newContent);
8503
+ console.error(`Wrote to ${path}`);
8504
+ } else {
8505
+ console.log(changelogEntry);
8506
+ }
8507
+ }
8508
+ var import_fs5, import_path5, CONVENTIONAL_TYPE_RE;
8509
+ var init_changelog = __esm({
8510
+ "src/commands/changelog.ts"() {
8511
+ "use strict";
8512
+ import_fs5 = require("fs");
8513
+ import_path5 = require("path");
8514
+ init_config();
8515
+ init_api();
8516
+ init_git();
8517
+ CONVENTIONAL_TYPE_RE = /^(feat|fix|docs|style|refactor|perf|test|chore)(\([^)]+\))?!?:\s+/i;
8518
+ }
8519
+ });
8520
+
7903
8521
  // src/monorepo.ts
7904
8522
  var monorepo_exports = {};
7905
8523
  __export(monorepo_exports, {
@@ -7909,7 +8527,7 @@ __export(monorepo_exports, {
7909
8527
  });
7910
8528
  function findGitRoot(start) {
7911
8529
  try {
7912
- return (0, import_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
8530
+ return (0, import_child_process5.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
7913
8531
  encoding: "utf-8",
7914
8532
  cwd: start,
7915
8533
  stdio: ["pipe", "pipe", "pipe"]
@@ -7919,19 +8537,19 @@ function findGitRoot(start) {
7919
8537
  }
7920
8538
  }
7921
8539
  function detectWorkspace(cwd = findGitRoot(process.cwd())) {
7922
- const pnpmWs = (0, import_path4.join)(cwd, "pnpm-workspace.yaml");
7923
- if ((0, import_fs4.existsSync)(pnpmWs)) {
7924
- const content = (0, import_fs4.readFileSync)(pnpmWs, "utf-8");
8540
+ const pnpmWs = (0, import_path6.join)(cwd, "pnpm-workspace.yaml");
8541
+ if ((0, import_fs6.existsSync)(pnpmWs)) {
8542
+ const content = (0, import_fs6.readFileSync)(pnpmWs, "utf-8");
7925
8543
  const match = content.match(/packages:\s*\n((?:\s+-\s+.+\n?)*)/);
7926
8544
  if (match) {
7927
8545
  const packages = match[1].split("\n").map((l) => l.replace(/^\s+-\s+/, "").replace(/["']/g, "").trim()).filter(Boolean);
7928
8546
  return { type: "pnpm", packages, root: cwd };
7929
8547
  }
7930
8548
  }
7931
- const lerna = (0, import_path4.join)(cwd, "lerna.json");
7932
- if ((0, import_fs4.existsSync)(lerna)) {
8549
+ const lerna = (0, import_path6.join)(cwd, "lerna.json");
8550
+ if ((0, import_fs6.existsSync)(lerna)) {
7933
8551
  try {
7934
- const config2 = JSON.parse((0, import_fs4.readFileSync)(lerna, "utf-8"));
8552
+ const config2 = JSON.parse((0, import_fs6.readFileSync)(lerna, "utf-8"));
7935
8553
  return {
7936
8554
  type: "lerna",
7937
8555
  packages: config2.packages ?? ["packages/*"],
@@ -7940,18 +8558,18 @@ function detectWorkspace(cwd = findGitRoot(process.cwd())) {
7940
8558
  } catch {
7941
8559
  }
7942
8560
  }
7943
- if ((0, import_fs4.existsSync)((0, import_path4.join)(cwd, "nx.json"))) {
8561
+ if ((0, import_fs6.existsSync)((0, import_path6.join)(cwd, "nx.json"))) {
7944
8562
  return {
7945
8563
  type: "nx",
7946
8564
  packages: ["packages/*", "apps/*", "libs/*"],
7947
8565
  root: cwd
7948
8566
  };
7949
8567
  }
7950
- if ((0, import_fs4.existsSync)((0, import_path4.join)(cwd, "turbo.json"))) {
7951
- const pkgPath2 = (0, import_path4.join)(cwd, "package.json");
7952
- if ((0, import_fs4.existsSync)(pkgPath2)) {
8568
+ if ((0, import_fs6.existsSync)((0, import_path6.join)(cwd, "turbo.json"))) {
8569
+ const pkgPath2 = (0, import_path6.join)(cwd, "package.json");
8570
+ if ((0, import_fs6.existsSync)(pkgPath2)) {
7953
8571
  try {
7954
- const config2 = JSON.parse((0, import_fs4.readFileSync)(pkgPath2, "utf-8"));
8572
+ const config2 = JSON.parse((0, import_fs6.readFileSync)(pkgPath2, "utf-8"));
7955
8573
  if (config2.workspaces) {
7956
8574
  const ws = Array.isArray(config2.workspaces) ? config2.workspaces : config2.workspaces.packages ?? [];
7957
8575
  return { type: "turbo", packages: ws, root: cwd };
@@ -7960,10 +8578,10 @@ function detectWorkspace(cwd = findGitRoot(process.cwd())) {
7960
8578
  }
7961
8579
  }
7962
8580
  }
7963
- const pkgPath = (0, import_path4.join)(cwd, "package.json");
7964
- if ((0, import_fs4.existsSync)(pkgPath)) {
8581
+ const pkgPath = (0, import_path6.join)(cwd, "package.json");
8582
+ if ((0, import_fs6.existsSync)(pkgPath)) {
7965
8583
  try {
7966
- const config2 = JSON.parse((0, import_fs4.readFileSync)(pkgPath, "utf-8"));
8584
+ const config2 = JSON.parse((0, import_fs6.readFileSync)(pkgPath, "utf-8"));
7967
8585
  if (config2.workspaces) {
7968
8586
  const ws = Array.isArray(config2.workspaces) ? config2.workspaces : config2.workspaces.packages ?? [];
7969
8587
  return { type: "npm", packages: ws, root: cwd };
@@ -8006,8 +8624,8 @@ function matchGlobPattern(rel, pattern) {
8006
8624
  return null;
8007
8625
  }
8008
8626
  function getPackageForFile(filePath, workspace) {
8009
- const absPath = filePath.startsWith("/") ? filePath : (0, import_path4.join)(workspace.root, filePath);
8010
- const rel = (0, import_path4.relative)(workspace.root, absPath);
8627
+ const absPath = filePath.startsWith("/") ? filePath : (0, import_path6.join)(workspace.root, filePath);
8628
+ const rel = (0, import_path6.relative)(workspace.root, absPath);
8011
8629
  for (const pattern of workspace.packages) {
8012
8630
  const packageName = matchGlobPattern(rel, pattern);
8013
8631
  if (packageName) return packageName;
@@ -8017,7 +8635,7 @@ function getPackageForFile(filePath, workspace) {
8017
8635
  function autoDetectScope(stagedFiles, workspace) {
8018
8636
  const packages = /* @__PURE__ */ new Set();
8019
8637
  for (const file of stagedFiles) {
8020
- const filePath = file.startsWith("/") ? file : (0, import_path4.join)(workspace.root, file);
8638
+ const filePath = file.startsWith("/") ? file : (0, import_path6.join)(workspace.root, file);
8021
8639
  const pkg = getPackageForFile(filePath, workspace);
8022
8640
  if (pkg) packages.add(pkg);
8023
8641
  }
@@ -8030,477 +8648,126 @@ function autoDetectScope(stagedFiles, workspace) {
8030
8648
  }
8031
8649
  return null;
8032
8650
  }
8033
- var import_child_process3, import_fs4, import_path4;
8651
+ var import_child_process5, import_fs6, import_path6;
8034
8652
  var init_monorepo = __esm({
8035
8653
  "src/monorepo.ts"() {
8036
8654
  "use strict";
8037
- import_child_process3 = require("child_process");
8038
- import_fs4 = require("fs");
8039
- import_path4 = require("path");
8655
+ import_child_process5 = require("child_process");
8656
+ import_fs6 = require("fs");
8657
+ import_path6 = require("path");
8040
8658
  }
8041
8659
  });
8042
8660
 
8043
- // src/commands/login.ts
8044
- var login_exports = {};
8045
- __export(login_exports, {
8046
- runLogin: () => runLogin
8047
- });
8048
- function openBrowser(url) {
8049
- try {
8050
- if ((0, import_os3.platform)() === "darwin") {
8051
- (0, import_child_process4.execFileSync)("open", [url], { stdio: "pipe" });
8052
- return true;
8053
- }
8054
- if ((0, import_os3.platform)() === "linux") {
8055
- (0, import_child_process4.execFileSync)("xdg-open", [url], { stdio: "pipe" });
8056
- return true;
8057
- }
8058
- if ((0, import_os3.platform)() === "win32") {
8059
- (0, import_child_process4.execFileSync)("cmd", ["/c", "start", "", url], { stdio: "pipe" });
8060
- return true;
8061
- }
8062
- } catch {
8063
- }
8064
- return false;
8661
+ // src/commands/changeset.ts
8662
+ var changeset_exports = {};
8663
+ __export(changeset_exports, {
8664
+ changeset: () => changeset,
8665
+ formatChangesetFile: () => formatChangesetFile,
8666
+ generateSlug: () => generateSlug,
8667
+ mapFilesToPackages: () => mapFilesToPackages
8668
+ });
8669
+ function generateSlug() {
8670
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
8671
+ return `${pick(ADJECTIVES)}-${pick(ANIMALS)}-${pick(VERBS)}`;
8065
8672
  }
8066
- async function runLogin() {
8067
- const codeRes = await fetch(`${API_URL}/api/auth/device/code`, {
8068
- method: "POST",
8069
- headers: { "Content-Type": "application/json" },
8070
- body: JSON.stringify({ client_id: CLIENT_ID })
8071
- });
8072
- if (!codeRes.ok) {
8073
- const err = await codeRes.json().catch(() => ({ error: codeRes.statusText }));
8074
- throw new Error(err.error ?? "Failed to start device flow");
8075
- }
8076
- const codeData = await codeRes.json();
8077
- const { device_code, user_code, verification_uri_complete, interval = 5 } = codeData;
8078
- if (!device_code || !user_code) {
8079
- throw new Error("Server did not return device codes");
8080
- }
8081
- console.log("Opening browser to sign in...");
8082
- console.log("");
8083
- console.log(` Your code: ${user_code}`);
8084
- console.log("");
8085
- const authUrl = verification_uri_complete ?? `${DASHBOARD_URL}/device?user_code=${encodeURIComponent(user_code)}`;
8086
- const opened = openBrowser(authUrl);
8087
- if (!opened) {
8088
- console.log("Could not open browser. Please visit:");
8089
- console.log(authUrl);
8090
- console.log("");
8091
- }
8092
- let frame = 0;
8093
- const spinner = setInterval(() => {
8094
- const elapsed = Math.floor((Date.now() - startTime) / 1e3);
8095
- process.stderr.write(
8096
- `\r${SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]} Waiting for authorization... (${elapsed}s)`
8097
- );
8098
- }, 80);
8099
- let pollingInterval = interval * 1e3;
8100
- const startTime = Date.now();
8101
- try {
8102
- while (Date.now() - startTime < DEVICE_FLOW_TIMEOUT) {
8103
- await new Promise((r) => setTimeout(r, pollingInterval));
8673
+ function mapFilesToPackages(files, workspace) {
8674
+ const dirToName = /* @__PURE__ */ new Map();
8675
+ for (const file of files) {
8676
+ const dirName = getPackageForFile(file, workspace);
8677
+ if (!dirName || dirToName.has(dirName)) continue;
8678
+ for (const pattern of workspace.packages) {
8679
+ const hasGlob = /\*/.test(pattern);
8680
+ const dir = pattern.replace(/\/?\*\*?$/, "").replace(/\/$/, "");
8681
+ const pkgJsonPath = hasGlob ? (0, import_path7.join)(workspace.root, dir, dirName, "package.json") : (0, import_path7.join)(workspace.root, pattern.replace(/\/$/, ""), "package.json");
8104
8682
  try {
8105
- const tokenRes = await fetch(`${API_URL}/api/auth/device/token`, {
8106
- method: "POST",
8107
- headers: { "Content-Type": "application/json" },
8108
- body: JSON.stringify({
8109
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
8110
- device_code,
8111
- client_id: CLIENT_ID
8112
- })
8113
- });
8114
- const tokenData = await tokenRes.json();
8115
- if (tokenData.access_token) {
8116
- saveApiKey(tokenData.access_token);
8117
- process.stderr.write("\r\x1B[2K");
8118
- console.log("Successfully logged in!");
8119
- return;
8120
- }
8121
- if (tokenData.error) {
8122
- switch (tokenData.error) {
8123
- case "authorization_pending":
8124
- break;
8125
- // continue polling
8126
- case "slow_down":
8127
- pollingInterval += 5e3;
8128
- break;
8129
- case "access_denied":
8130
- process.stderr.write("\r\x1B[2K");
8131
- console.error("Authorization was denied.");
8132
- process.exit(1);
8133
- break;
8134
- case "expired_token":
8135
- process.stderr.write("\r\x1B[2K");
8136
- console.error("Device code expired. Please try again.");
8137
- process.exit(1);
8138
- break;
8139
- default:
8140
- process.stderr.write("\r\x1B[2K");
8141
- console.error(`Error: ${tokenData.error_description ?? tokenData.error}`);
8142
- process.exit(1);
8143
- }
8144
- }
8683
+ const pkg = JSON.parse((0, import_fs7.readFileSync)(pkgJsonPath, "utf-8"));
8684
+ dirToName.set(dirName, pkg.name ?? dirName);
8685
+ break;
8145
8686
  } catch {
8146
8687
  }
8147
8688
  }
8148
- process.stderr.write("\r\x1B[2K");
8149
- console.error("Login timed out. Please try again.");
8150
- process.exit(1);
8151
- } finally {
8152
- clearInterval(spinner);
8689
+ if (!dirToName.has(dirName)) {
8690
+ dirToName.set(dirName, dirName);
8691
+ }
8153
8692
  }
8693
+ return dirToName;
8154
8694
  }
8155
- var import_child_process4, import_os3, API_URL, DASHBOARD_URL, CLIENT_ID, SPINNER_FRAMES;
8156
- var init_login = __esm({
8157
- "src/commands/login.ts"() {
8158
- "use strict";
8159
- import_child_process4 = require("child_process");
8160
- import_os3 = require("os");
8161
- init_config();
8162
- init_dist();
8163
- API_URL = process.env.QC_API_URL ?? DEFAULT_API_URL;
8164
- DASHBOARD_URL = "https://app.quikcommit.dev";
8165
- CLIENT_ID = "qc-cli";
8166
- SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8167
- }
8168
- });
8695
+ function formatChangesetFile(packages, summary) {
8696
+ const frontmatter = packages.map((p) => `"${p.name}": ${p.bump}`).join("\n");
8697
+ return `---
8698
+ ${frontmatter}
8699
+ ---
8169
8700
 
8170
- // src/commands/logout.ts
8171
- var logout_exports = {};
8172
- __export(logout_exports, {
8173
- runLogout: () => runLogout
8174
- });
8175
- function runLogout() {
8176
- clearApiKey();
8177
- console.log("Logged out. Credentials cleared.");
8701
+ ${summary}
8702
+ `;
8178
8703
  }
8179
- var init_logout = __esm({
8180
- "src/commands/logout.ts"() {
8181
- "use strict";
8182
- init_config();
8183
- }
8184
- });
8185
-
8186
- // src/commands/status.ts
8187
- var status_exports = {};
8188
- __export(status_exports, {
8189
- runStatus: () => runStatus
8190
- });
8191
- async function runStatus(apiKeyFlag) {
8192
- const apiKey = apiKeyFlag ?? getApiKey();
8704
+ async function prompt(rl, question) {
8705
+ return new Promise((resolve) => rl.question(question, resolve));
8706
+ }
8707
+ async function changeset(options) {
8708
+ const base = options.base ?? "main";
8709
+ const apiKey = getApiKey();
8193
8710
  if (!apiKey) {
8194
- console.log("Not logged in. Run `qc login` to authenticate.");
8195
- return;
8711
+ console.error("Error: Not authenticated. Run `qc login` first.");
8712
+ process.exit(1);
8196
8713
  }
8197
- console.log("Logged in: yes");
8198
- console.log(` API key: ...${apiKey.slice(-4)}`);
8199
- const client = new ApiClient({ apiKey });
8200
- const usage = await client.getUsage();
8201
- if (usage) {
8202
- console.log(`Plan: ${usage.plan}`);
8203
- console.log(`Usage: ${usage.commit_count}/${usage.limit} commits this period`);
8204
- console.log(`Remaining: ${usage.remaining}`);
8205
- } else {
8206
- console.log("Usage: (unable to fetch)");
8714
+ const { detectWorkspace: detectWorkspace2 } = await Promise.resolve().then(() => (init_monorepo(), monorepo_exports));
8715
+ const workspace = detectWorkspace2();
8716
+ if (!workspace) {
8717
+ console.error(
8718
+ "No workspace packages found. Is this a pnpm monorepo?"
8719
+ );
8720
+ process.exit(1);
8207
8721
  }
8208
- }
8209
- var init_status = __esm({
8210
- "src/commands/status.ts"() {
8211
- "use strict";
8212
- init_config();
8213
- init_api();
8722
+ const changedFiles = getChangedFilesSince(base);
8723
+ if (changedFiles.length === 0) {
8724
+ console.error(`No changes detected vs ${base}.`);
8725
+ process.exit(1);
8214
8726
  }
8215
- });
8216
-
8217
- // src/commands/pr.ts
8218
- var pr_exports = {};
8219
- __export(pr_exports, {
8220
- pr: () => pr
8221
- });
8222
- function findPullRequestTemplate(gitRoot) {
8223
- const fileCandidates = [
8224
- (0, import_path5.join)(gitRoot, ".github", "pull_request_template.md"),
8225
- (0, import_path5.join)(gitRoot, ".github", "PULL_REQUEST_TEMPLATE.md"),
8226
- (0, import_path5.join)(gitRoot, "pull_request_template.md")
8227
- ];
8228
- for (const p of fileCandidates) {
8727
+ const packageMap = mapFilesToPackages(changedFiles, workspace);
8728
+ const packageNames = Array.from(packageMap.values());
8729
+ if (packageNames.length === 0) {
8730
+ console.error("No workspace packages detected in changed files.");
8731
+ process.exit(1);
8732
+ }
8733
+ const commits = getOnlineLog(base);
8734
+ const diff = getFullDiff(base);
8735
+ const commitCount = commits.split("\n").filter(Boolean).length;
8736
+ console.error(
8737
+ `Analyzing changes vs ${base}... ${commitCount} commit(s), ${packageNames.length} package(s) changed`
8738
+ );
8739
+ const client = new ApiClient({ apiKey });
8740
+ let result;
8741
+ const msg = (e) => e instanceof Error ? e.message : String(e);
8742
+ const isTransient = (m) => /invalid json|no changeset|unexpected response|ai worker|timeout|502|503|504/i.test(m);
8743
+ let attempts = 0;
8744
+ while (true) {
8229
8745
  try {
8230
- if ((0, import_fs5.existsSync)(p) && (0, import_fs5.statSync)(p).isFile()) {
8231
- return { path: p, content: (0, import_fs5.readFileSync)(p, "utf-8") };
8746
+ result = await client.generateChangeset({
8747
+ diff,
8748
+ packages: packageNames,
8749
+ commits,
8750
+ model: options.model
8751
+ });
8752
+ break;
8753
+ } catch (err) {
8754
+ const m = msg(err);
8755
+ if (!isTransient(m)) {
8756
+ console.error(m);
8757
+ process.exit(1);
8232
8758
  }
8233
- } catch {
8759
+ if (attempts === 0) {
8760
+ attempts++;
8761
+ continue;
8762
+ }
8763
+ console.error(m);
8764
+ process.exit(1);
8234
8765
  }
8235
8766
  }
8236
- const multiDir = (0, import_path5.join)(gitRoot, ".github", "PULL_REQUEST_TEMPLATE");
8237
- try {
8238
- if ((0, import_fs5.existsSync)(multiDir) && (0, import_fs5.statSync)(multiDir).isDirectory()) {
8239
- const names = (0, import_fs5.readdirSync)(multiDir).filter((f) => f.toLowerCase().endsWith(".md")).sort();
8240
- if (names.length > 0) {
8241
- const p = (0, import_path5.join)(multiDir, names[0]);
8242
- if ((0, import_fs5.statSync)(p).isFile()) {
8243
- return { path: p, content: (0, import_fs5.readFileSync)(p, "utf-8") };
8244
- }
8245
- }
8246
- }
8247
- } catch {
8248
- }
8249
- return void 0;
8250
- }
8251
- async function pr(options) {
8252
- const base = options.base ?? "main";
8253
- const commits = getBranchCommits(base);
8254
- const diffStat = getDiffStat(base);
8255
- const gitRoot = getGitRoot();
8256
- const templateHit = findPullRequestTemplate(gitRoot);
8257
- let prTemplate;
8258
- if (templateHit) {
8259
- prTemplate = templateHit.content.substring(0, 16 * 1024);
8260
- console.error(`[qc] Using PR template from ${(0, import_path5.relative)(gitRoot, templateHit.path)}`);
8261
- }
8262
- const currentBranch = getCurrentBranch().slice(0, MAX_PR_CURRENT_BRANCH_CHARS);
8263
- if (commits.length === 0) {
8264
- console.error(`No commits found on this branch vs ${base}`);
8265
- process.exit(1);
8266
- }
8267
- const commitlintRules = await detectCommitlintRules();
8268
- console.error(`Generating PR description from ${commits.length} commits...`);
8269
- const apiKey = getApiKey();
8270
- if (!apiKey) {
8271
- console.error("Error: Not authenticated. Run `qc login` first.");
8272
- process.exit(1);
8273
- }
8274
- const client = new ApiClient({ apiKey });
8275
- const result = await client.generatePR(
8276
- {
8277
- commits,
8278
- diff_stat: diffStat,
8279
- base_branch: base,
8280
- current_branch: currentBranch,
8281
- pr_template: prTemplate,
8282
- rules: commitlintRules
8283
- },
8284
- options.model
8285
- );
8286
- const trimmedTitle = result.title.trim();
8287
- if (trimmedTitle) {
8288
- console.log(`
8289
- Title: ${trimmedTitle}
8290
- `);
8291
- }
8292
- console.log(result.message + "\n");
8293
- if (options.create) {
8294
- try {
8295
- const prTitle = trimmedTitle || result.message.split("\n").find((l) => l.trim()) || result.message.substring(0, 72).trim();
8296
- (0, import_child_process5.execFileSync)("gh", ["pr", "create", "--title", prTitle, "--body", result.message], {
8297
- stdio: "inherit"
8298
- });
8299
- } catch {
8300
- console.error("Error: `gh` CLI not found or failed. Install from https://cli.github.com/");
8301
- process.exit(1);
8302
- }
8303
- }
8304
- }
8305
- var import_child_process5, import_fs5, import_path5;
8306
- var init_pr = __esm({
8307
- "src/commands/pr.ts"() {
8308
- "use strict";
8309
- import_child_process5 = require("child_process");
8310
- import_fs5 = require("fs");
8311
- import_path5 = require("path");
8312
- init_dist();
8313
- init_config();
8314
- init_api();
8315
- init_commitlint();
8316
- init_git();
8317
- }
8318
- });
8319
-
8320
- // src/commands/changelog.ts
8321
- var changelog_exports = {};
8322
- __export(changelog_exports, {
8323
- changelog: () => changelog
8324
- });
8325
- function parseCommitType(subject) {
8326
- const match = subject.match(CONVENTIONAL_TYPE_RE);
8327
- return match ? match[1].toLowerCase() : "chore";
8328
- }
8329
- function groupCommitsByType(commits) {
8330
- const byType = {};
8331
- for (const { subject } of commits) {
8332
- const type = parseCommitType(subject);
8333
- if (!byType[type]) byType[type] = [];
8334
- byType[type].push(subject);
8335
- }
8336
- return byType;
8337
- }
8338
- async function changelog(options) {
8339
- const fromRef = options.from ?? getLatestTag();
8340
- const toRef = options.to ?? "HEAD";
8341
- if (!fromRef) {
8342
- console.error("Error: No git tag found. Use --from <ref> to specify a starting point.");
8343
- process.exit(1);
8344
- }
8345
- const commits = getCommitsSince(fromRef, toRef);
8346
- if (commits.length === 0) {
8347
- console.error(`No commits found between ${fromRef} and ${toRef}`);
8348
- process.exit(1);
8349
- }
8350
- const commitsByType = groupCommitsByType(commits);
8351
- const apiKey = getApiKey();
8352
- if (!apiKey) {
8353
- console.error("Error: Not authenticated. Run `qc login` first.");
8354
- process.exit(1);
8355
- }
8356
- const client = new ApiClient({ apiKey });
8357
- const result = await client.generateChangelog(
8358
- {
8359
- commits_by_type: commitsByType,
8360
- from_tag: fromRef,
8361
- to_ref: toRef
8362
- },
8363
- options.model
8364
- );
8365
- const version = options.version ?? (/^v?\d/.test(toRef) && toRef !== "HEAD" ? toRef.replace(/^v/, "") : null) ?? `${fromRef.replace(/^v/, "")}-next`;
8366
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8367
- const header = `## [${version}] - ${date}
8368
-
8369
- `;
8370
- const changelogEntry = header + result.message;
8371
- if (options.write) {
8372
- const path = (0, import_path6.join)(getGitRoot(), "CHANGELOG.md");
8373
- const existing = (0, import_fs6.existsSync)(path) ? (0, import_fs6.readFileSync)(path, "utf-8") : "";
8374
- const newContent = changelogEntry + (existing ? "\n\n" + existing : "");
8375
- (0, import_fs6.writeFileSync)(path, newContent);
8376
- console.error(`Wrote to ${path}`);
8377
- } else {
8378
- console.log(changelogEntry);
8379
- }
8380
- }
8381
- var import_fs6, import_path6, CONVENTIONAL_TYPE_RE;
8382
- var init_changelog = __esm({
8383
- "src/commands/changelog.ts"() {
8384
- "use strict";
8385
- import_fs6 = require("fs");
8386
- import_path6 = require("path");
8387
- init_config();
8388
- init_api();
8389
- init_git();
8390
- CONVENTIONAL_TYPE_RE = /^(feat|fix|docs|style|refactor|perf|test|chore)(\([^)]+\))?!?:\s+/i;
8391
- }
8392
- });
8393
-
8394
- // src/commands/changeset.ts
8395
- var changeset_exports = {};
8396
- __export(changeset_exports, {
8397
- changeset: () => changeset,
8398
- formatChangesetFile: () => formatChangesetFile,
8399
- generateSlug: () => generateSlug,
8400
- mapFilesToPackages: () => mapFilesToPackages
8401
- });
8402
- function generateSlug() {
8403
- const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
8404
- return `${pick(ADJECTIVES)}-${pick(ANIMALS)}-${pick(VERBS)}`;
8405
- }
8406
- function mapFilesToPackages(files, workspace) {
8407
- const dirToName = /* @__PURE__ */ new Map();
8408
- for (const file of files) {
8409
- const dirName = getPackageForFile(file, workspace);
8410
- if (!dirName || dirToName.has(dirName)) continue;
8411
- for (const pattern of workspace.packages) {
8412
- const hasGlob = /\*/.test(pattern);
8413
- const dir = pattern.replace(/\/?\*\*?$/, "").replace(/\/$/, "");
8414
- const pkgJsonPath = hasGlob ? (0, import_path7.join)(workspace.root, dir, dirName, "package.json") : (0, import_path7.join)(workspace.root, pattern.replace(/\/$/, ""), "package.json");
8415
- try {
8416
- const pkg = JSON.parse((0, import_fs7.readFileSync)(pkgJsonPath, "utf-8"));
8417
- dirToName.set(dirName, pkg.name ?? dirName);
8418
- break;
8419
- } catch {
8420
- }
8421
- }
8422
- if (!dirToName.has(dirName)) {
8423
- dirToName.set(dirName, dirName);
8424
- }
8425
- }
8426
- return dirToName;
8427
- }
8428
- function formatChangesetFile(packages, summary) {
8429
- const frontmatter = packages.map((p) => `"${p.name}": ${p.bump}`).join("\n");
8430
- return `---
8431
- ${frontmatter}
8432
- ---
8433
-
8434
- ${summary}
8435
- `;
8436
- }
8437
- async function prompt(rl, question) {
8438
- return new Promise((resolve) => rl.question(question, resolve));
8439
- }
8440
- async function changeset(options) {
8441
- const base = options.base ?? "main";
8442
- const apiKey = getApiKey();
8443
- if (!apiKey) {
8444
- console.error("Error: Not authenticated. Run `qc login` first.");
8445
- process.exit(1);
8446
- }
8447
- const { detectWorkspace: detectWorkspace2 } = await Promise.resolve().then(() => (init_monorepo(), monorepo_exports));
8448
- const workspace = detectWorkspace2();
8449
- if (!workspace) {
8450
- console.error(
8451
- "No workspace packages found. Is this a pnpm monorepo?"
8452
- );
8453
- process.exit(1);
8454
- }
8455
- const changedFiles = getChangedFilesSince(base);
8456
- if (changedFiles.length === 0) {
8457
- console.error(`No changes detected vs ${base}.`);
8458
- process.exit(1);
8459
- }
8460
- const packageMap = mapFilesToPackages(changedFiles, workspace);
8461
- const packageNames = Array.from(packageMap.values());
8462
- if (packageNames.length === 0) {
8463
- console.error("No workspace packages detected in changed files.");
8464
- process.exit(1);
8465
- }
8466
- const commits = getOnlineLog(base);
8467
- const diff = getFullDiff(base);
8468
- const commitCount = commits.split("\n").filter(Boolean).length;
8469
- console.error(
8470
- `Analyzing changes vs ${base}... ${commitCount} commit(s), ${packageNames.length} package(s) changed`
8471
- );
8472
- const client = new ApiClient({ apiKey });
8473
- let result;
8474
- const msg = (e) => e instanceof Error ? e.message : String(e);
8475
- const isTransient = (m) => /invalid json|no changeset|unexpected response|ai worker|timeout|502|503|504/i.test(m);
8476
- let attempts = 0;
8477
- while (true) {
8478
- try {
8479
- result = await client.generateChangeset({
8480
- diff,
8481
- packages: packageNames,
8482
- commits,
8483
- model: options.model
8484
- });
8485
- break;
8486
- } catch (err) {
8487
- const m = msg(err);
8488
- if (!isTransient(m)) {
8489
- console.error(m);
8490
- process.exit(1);
8491
- }
8492
- if (attempts === 0) {
8493
- attempts++;
8494
- continue;
8495
- }
8496
- console.error(m);
8497
- process.exit(1);
8498
- }
8499
- }
8500
- const resultNames = new Set(result.packages.map((p) => p.name));
8501
- for (const name of packageNames) {
8502
- if (!resultNames.has(name)) {
8503
- result.packages.push({ name, bump: "patch", reason: "included in changeset" });
8767
+ const resultNames = new Set(result.packages.map((p) => p.name));
8768
+ for (const name of packageNames) {
8769
+ if (!resultNames.has(name)) {
8770
+ result.packages.push({ name, bump: "patch", reason: "included in changeset" });
8504
8771
  }
8505
8772
  }
8506
8773
  console.log("");
@@ -8697,6 +8964,1561 @@ var init_changeset = __esm({
8697
8964
  }
8698
8965
  });
8699
8966
 
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
+ );
8974
+ }
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 {
8984
+ }
8985
+ }
8986
+ throw err;
8987
+ }
8988
+ try {
8989
+ resetHard(upstream);
8990
+ } catch (err) {
8991
+ try {
8992
+ deleteBranch(opts.newBranch);
8993
+ } catch {
8994
+ }
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)}`
9003
+ );
9004
+ }
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
+ };
9036
+ }
9037
+ var init_branch_rescue = __esm({
9038
+ "src/branch-rescue.ts"() {
9039
+ "use strict";
9040
+ init_git();
9041
+ }
9042
+ });
9043
+
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);
9055
+ }
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);
9066
+ }
9067
+ if (opts.detectDefault && opts.defaultBranch) {
9068
+ set.add(opts.defaultBranch);
9069
+ }
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"];
9077
+ }
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" };
9097
+ }
9098
+ const commitsAhead = getCommitsAheadOfUpstream(branch);
9099
+ const mode = commitsAhead > 0 ? "rescue" : "uncommitted";
9100
+ return { isProtected: true, branch, commitsAhead, mode };
9101
+ }
9102
+ var init_protected_branch_guard = __esm({
9103
+ "src/protected-branch-guard.ts"() {
9104
+ "use strict";
9105
+ init_branch_detect();
9106
+ init_git();
9107
+ }
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}`);
9142
+ }
9143
+ candidate = s;
9144
+ }
9145
+ if (options.skipUniqueness) {
9146
+ return candidate;
9147
+ }
9148
+ return ensureUniqueName(candidate, exists);
9149
+ }
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;
9807
+ }
9808
+ }
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);
9943
+ }
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" } };
9949
+ }
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;
10079
+ }
10080
+ message = refineResult.message;
10081
+ }
10082
+ }
10083
+ if (args.messageOnly) {
10084
+ console.log(message);
10085
+ return;
10086
+ }
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) {
10106
+ return;
10107
+ }
10108
+ if (args.confirm) {
10109
+ const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
10110
+ if (confirmResult.action === "abort") {
10111
+ return;
10112
+ }
10113
+ }
10114
+ gitCommit(message);
10115
+ if (args.push) {
10116
+ gitPush();
10117
+ }
10118
+ }
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";
10147
+ }
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;
10183
+ }
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
+ );
10188
+ }
10189
+ let res;
10190
+ try {
10191
+ res = await fetch(url, {
10192
+ method: "POST",
10193
+ headers,
10194
+ body: JSON.stringify(body)
10195
+ });
10196
+ } catch {
10197
+ const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
10198
+ return ensureUniqueName(fallback.name, branchExists);
10199
+ }
10200
+ if (!res.ok) {
10201
+ const fallback = deterministicBranchName({ files: opts.changes?.split("\n").filter(Boolean), description: opts.description });
10202
+ return ensureUniqueName(fallback.name, branchExists);
10203
+ }
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);
10221
+ }
10222
+ return ensureUniqueName(sanitized, branchExists);
10223
+ }
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}`);
10263
+ }
10264
+ }
10265
+ var import_fs8, import_path8, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
10266
+ var init_local = __esm({
10267
+ "src/local.ts"() {
10268
+ "use strict";
10269
+ import_fs8 = require("fs");
10270
+ import_path8 = require("path");
10271
+ import_os4 = require("os");
10272
+ init_config();
10273
+ init_dist();
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: ""
10288
+ };
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"
10295
+ };
10296
+ }
10297
+ });
10298
+
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;
10308
+ }
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();
10370
+ }
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();
10398
+ }
10399
+ }
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;
10447
+ }
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();
10461
+ }
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.");
10480
+ }
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();
10489
+ }
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;
10495
+ }
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}`);
10502
+ }
10503
+ if (opts.push) {
10504
+ gitPushSetUpstream(final);
10505
+ log.success(`pushed origin/${final}`);
10506
+ }
10507
+ }
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();
10519
+ }
10520
+ });
10521
+
8700
10522
  // src/commands/init.ts
8701
10523
  var init_exports = {};
8702
10524
  __export(init_exports, {
@@ -8712,12 +10534,12 @@ function init(options) {
8712
10534
  console.error("Error: Not a git repository");
8713
10535
  process.exit(1);
8714
10536
  }
8715
- const hookPath = (0, import_path8.join)(hooksDir, "prepare-commit-msg");
10537
+ const hookPath = (0, import_path9.join)(hooksDir, "prepare-commit-msg");
8716
10538
  if (options.uninstall) {
8717
- if ((0, import_fs8.existsSync)(hookPath)) {
8718
- const content = (0, import_fs8.readFileSync)(hookPath, "utf-8");
10539
+ if ((0, import_fs9.existsSync)(hookPath)) {
10540
+ const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
8719
10541
  if (content.includes("Quikcommit")) {
8720
- (0, import_fs8.unlinkSync)(hookPath);
10542
+ (0, import_fs9.unlinkSync)(hookPath);
8721
10543
  console.log("Quikcommit hook removed.");
8722
10544
  } else {
8723
10545
  console.log("Hook exists but was not installed by Quikcommit. Skipping.");
@@ -8727,8 +10549,8 @@ function init(options) {
8727
10549
  }
8728
10550
  return;
8729
10551
  }
8730
- if ((0, import_fs8.existsSync)(hookPath)) {
8731
- const content = (0, import_fs8.readFileSync)(hookPath, "utf-8");
10552
+ if ((0, import_fs9.existsSync)(hookPath)) {
10553
+ const content = (0, import_fs9.readFileSync)(hookPath, "utf-8");
8732
10554
  if (content.includes("Quikcommit")) {
8733
10555
  console.log("Quikcommit hook is already installed.");
8734
10556
  return;
@@ -8738,17 +10560,17 @@ function init(options) {
8738
10560
  );
8739
10561
  process.exit(1);
8740
10562
  }
8741
- (0, import_fs8.writeFileSync)(hookPath, HOOK_CONTENT);
8742
- (0, import_fs8.chmodSync)(hookPath, 493);
10563
+ (0, import_fs9.writeFileSync)(hookPath, HOOK_CONTENT);
10564
+ (0, import_fs9.chmodSync)(hookPath, 493);
8743
10565
  console.log("Quikcommit hook installed.");
8744
10566
  console.log("Now just run `git commit` and a message will be generated automatically.");
8745
10567
  }
8746
- var import_fs8, import_path8, import_child_process6, HOOK_CONTENT;
10568
+ var import_fs9, import_path9, import_child_process6, HOOK_CONTENT;
8747
10569
  var init_init = __esm({
8748
10570
  "src/commands/init.ts"() {
8749
10571
  "use strict";
8750
- import_fs8 = require("fs");
8751
- import_path8 = require("path");
10572
+ import_fs9 = require("fs");
10573
+ import_path9 = require("path");
8752
10574
  import_child_process6 = require("child_process");
8753
10575
  HOOK_CONTENT = `#!/bin/sh
8754
10576
  # Quikcommit - auto-generate commit messages
@@ -8823,10 +10645,10 @@ function detectLocalCommitlintRules() {
8823
10645
  "commitlint.config.mjs"
8824
10646
  ];
8825
10647
  for (const file of files) {
8826
- const path = (0, import_path9.join)(cwd, file);
8827
- if (!(0, import_fs9.existsSync)(path)) continue;
10648
+ const path = (0, import_path10.join)(cwd, file);
10649
+ if (!(0, import_fs10.existsSync)(path)) continue;
8828
10650
  try {
8829
- const content = (0, import_fs9.readFileSync)(path, "utf-8");
10651
+ const content = (0, import_fs10.readFileSync)(path, "utf-8");
8830
10652
  let parsed;
8831
10653
  if (file.endsWith(".json") || file === ".commitlintrc") {
8832
10654
  parsed = JSON.parse(content);
@@ -8838,10 +10660,10 @@ function detectLocalCommitlintRules() {
8838
10660
  } catch {
8839
10661
  }
8840
10662
  }
8841
- const pkgPath = (0, import_path9.join)(cwd, "package.json");
8842
- if ((0, import_fs9.existsSync)(pkgPath)) {
10663
+ const pkgPath = (0, import_path10.join)(cwd, "package.json");
10664
+ if ((0, import_fs10.existsSync)(pkgPath)) {
8843
10665
  try {
8844
- const content = (0, import_fs9.readFileSync)(pkgPath, "utf-8");
10666
+ const content = (0, import_fs10.readFileSync)(pkgPath, "utf-8");
8845
10667
  const pkg = JSON.parse(content);
8846
10668
  if (pkg.commitlint) {
8847
10669
  const rules = mapCommitlintToRules(pkg.commitlint);
@@ -8904,20 +10726,20 @@ async function team(subcommand, args) {
8904
10726
  process.exit(1);
8905
10727
  }
8906
10728
  }
8907
- var import_fs9, import_path9;
10729
+ var import_fs10, import_path10;
8908
10730
  var init_team = __esm({
8909
10731
  "src/commands/team.ts"() {
8910
10732
  "use strict";
8911
- import_fs9 = require("fs");
8912
- import_path9 = require("path");
10733
+ import_fs10 = require("fs");
10734
+ import_path10 = require("path");
8913
10735
  init_api();
8914
10736
  init_config();
8915
10737
  }
8916
10738
  });
8917
10739
 
8918
10740
  // src/commands/config.ts
8919
- var config_exports = {};
8920
- __export(config_exports, {
10741
+ var config_exports2 = {};
10742
+ __export(config_exports2, {
8921
10743
  config: () => config
8922
10744
  });
8923
10745
  function config(args) {
@@ -9030,459 +10852,698 @@ var init_upgrade = __esm({
9030
10852
  }
9031
10853
  });
9032
10854
 
9033
- // src/local.ts
9034
- var local_exports = {};
9035
- __export(local_exports, {
9036
- getLocalProviderConfig: () => getLocalProviderConfig,
9037
- runLocalCommit: () => runLocalCommit
9038
- });
9039
- function getLegacyProvider() {
9040
- try {
9041
- const p = (0, import_path10.join)(CONFIG_PATH2, "provider");
9042
- if ((0, import_fs10.existsSync)(p)) {
9043
- const v = (0, import_fs10.readFileSync)(p, "utf-8").trim().toLowerCase();
9044
- if (["ollama", "lmstudio", "openrouter", "custom", "cloudflare"].includes(v)) {
9045
- return v;
9046
- }
9047
- }
9048
- } catch {
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" };
9049
10872
  }
9050
- return null;
9051
- }
9052
- function getLegacyBaseUrl(provider) {
9053
- try {
9054
- const p = (0, import_path10.join)(CONFIG_PATH2, "base_url");
9055
- if ((0, import_fs10.existsSync)(p)) {
9056
- return (0, import_fs10.readFileSync)(p, "utf-8").trim();
9057
- }
9058
- } catch {
10873
+ log.error(
10874
+ `You're on ${state.branch} (a protected branch).` + (state.commitsAhead > 0 ? ` ${state.commitsAhead} commit(s) ahead of upstream.` : "")
10875
+ );
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);
9059
10888
  }
9060
- return PROVIDER_URLS[provider] ?? "";
9061
- }
9062
- function getLegacyModel(provider) {
9063
- try {
9064
- const p = (0, import_path10.join)(CONFIG_PATH2, "model");
9065
- if ((0, import_fs10.existsSync)(p)) {
9066
- const v = (0, import_fs10.readFileSync)(p, "utf-8").trim();
9067
- if (v) return v;
9068
- }
9069
- } catch {
10889
+ if (action === "continue" && usedConfigDefault) {
10890
+ log.dim("(continuing on protected branch per config `branch.defaultAction`)");
9070
10891
  }
9071
- return DEFAULT_MODELS[provider] ?? "";
9072
- }
9073
- function getLocalProviderConfig() {
9074
- const config2 = getConfig();
9075
- const provider = config2.provider ?? getLegacyProvider();
9076
- if (!provider) return null;
9077
- const baseUrl = config2.apiUrl ?? getLegacyBaseUrl(provider) ?? PROVIDER_URLS[provider] ?? "";
9078
- if (!baseUrl) return null;
9079
- const model = config2.model ?? getLegacyModel(provider) ?? DEFAULT_MODELS[provider];
9080
- const apiKey = provider === "openrouter" || provider === "custom" ? getApiKey() : null;
9081
- if (provider === "openrouter" && !apiKey) return null;
9082
- return { provider, baseUrl, model, apiKey };
9083
- }
9084
- function buildUserPrompt(changes, diff, rules) {
9085
- let prompt2 = `Generate a commit message for these changes:
9086
-
9087
- ## File changes:
9088
- <file_changes>
9089
- ${changes}
9090
- </file_changes>
9091
-
9092
- ## Diff:
9093
- <diff>
9094
- ${diff}
9095
- </diff>
9096
-
9097
- `;
9098
- if (rules && Object.keys(rules).length > 0) {
9099
- prompt2 += `Rules: ${JSON.stringify(rules)}
9100
-
9101
- `;
10892
+ if (action === "abort") {
10893
+ log.dim("aborted.");
10894
+ return { action: "abort" };
9102
10895
  }
9103
- prompt2 += `Important:
9104
- - Follow conventional commit format: <type>(<scope>): <subject>
9105
- - Response should be the commit message only, no explanations`;
9106
- return prompt2;
9107
- }
9108
- function buildRequest(provider, baseUrl, userContent, diff, changes, model, apiKey, rules) {
9109
- const headers = {
9110
- "Content-Type": "application/json"
9111
- };
9112
- if (apiKey) {
9113
- headers["Authorization"] = `Bearer ${apiKey}`;
10896
+ if (action === "continue") {
10897
+ return { action: "continue" };
9114
10898
  }
9115
- if (provider === "openrouter") {
9116
- headers["HTTP-Referer"] = "https://github.com/Quikcommit-Internal/public";
9117
- headers["X-Title"] = "qc - AI Commit Message Generator";
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
+ }
10937
+ } else {
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;
10950
+ }
10951
+ }
10952
+ } finally {
10953
+ spinner.stop();
9118
10954
  }
9119
- let url;
9120
- let body;
9121
- switch (provider) {
9122
- case "ollama":
9123
- url = `${baseUrl}/api/generate`;
9124
- body = {
9125
- model,
9126
- prompt: userContent,
9127
- stream: false,
9128
- options: {}
9129
- };
9130
- return { url, body, headers: { "Content-Type": "application/json" } };
9131
- case "lmstudio":
9132
- url = `${baseUrl}/chat/completions`;
9133
- body = {
9134
- model,
9135
- stream: false,
9136
- messages: [
9137
- {
9138
- role: "system",
9139
- content: "You are a git commit message generator. Create conventional commit messages."
9140
- },
9141
- { role: "user", content: userContent }
9142
- ]
9143
- };
9144
- return { url, body, headers: { "Content-Type": "application/json" } };
9145
- case "openrouter":
9146
- case "custom":
9147
- url = `${baseUrl}/chat/completions`;
9148
- body = {
9149
- model,
9150
- stream: false,
9151
- messages: [
9152
- {
9153
- role: "system",
9154
- content: "You are a git commit message generator. Create conventional commit messages."
9155
- },
9156
- { role: "user", content: userContent }
9157
- ]
9158
- };
9159
- return { url, body, headers };
9160
- case "cloudflare":
9161
- url = `${baseUrl.replace(/\/$/, "")}/commit`;
9162
- body = { diff, changes, rules };
9163
- return { url, body, headers: { "Content-Type": "application/json" } };
9164
- default:
9165
- throw new Error(`Unknown provider: ${provider}`);
10955
+ if (usedFallback) {
10956
+ log.dim("(used local fallback name; AI generation failed)");
9166
10957
  }
9167
- }
9168
- function parseResponse(provider, data) {
9169
- const r = data;
9170
- switch (provider) {
9171
- case "ollama":
9172
- return r.response ?? "";
9173
- case "lmstudio":
9174
- case "openrouter":
9175
- case "custom": {
9176
- const choices = r.choices;
9177
- return choices?.[0]?.message?.content ?? "";
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" };
9178
10975
  }
9179
- case "cloudflare":
9180
- return r.commit?.response ?? "";
9181
- default:
9182
- return "";
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" };
10983
+ }
10984
+ return { action: "done" };
10985
+ }
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();
9183
11000
  }
9184
11001
  }
9185
- async function runLocalCommit(messageOnly, push, modelFlag) {
11002
+ var import_promises2;
11003
+ var init_branch_guard = __esm({
11004
+ "src/branch-guard.ts"() {
11005
+ "use strict";
11006
+ import_promises2 = __toESM(require("node:readline/promises"));
11007
+ init_api();
11008
+ init_git();
11009
+ init_protected_branch_guard();
11010
+ init_branch_rescue();
11011
+ init_branch_name();
11012
+ init_ui();
11013
+ init_commit_helpers();
11014
+ }
11015
+ });
11016
+
11017
+ // src/commands/commit.ts
11018
+ var commit_exports = {};
11019
+ __export(commit_exports, {
11020
+ runCommit: () => runCommit
11021
+ });
11022
+ async function runCommit(args) {
11023
+ const { messageOnly, push, apiKey: apiKeyFlag, hookMode, model: modelFlag, all } = args;
11024
+ const silent = !!(hookMode || args.quiet);
11025
+ const ui2 = getUI();
11026
+ const log = silent ? createSilentLog() : ui2.log;
9186
11027
  if (!isGitRepo()) {
9187
- throw new Error("Not a git repository.");
11028
+ log.error("Not a git repository.");
11029
+ process.exit(1);
11030
+ }
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;
11052
+ if (all || config2.autoStage) {
11053
+ stageAll();
11054
+ const { files, total } = getShortStagedFiles();
11055
+ const fileList = total > 3 ? `${files.join(", ")}, +${total - 3} more` : files.join(", ");
11056
+ log.step(`staging working tree (${total} file(s))...`);
11057
+ if (fileList) log.dim(` ${fileList}`);
9188
11058
  }
9189
11059
  if (!hasStagedChanges()) {
9190
- throw new Error("No staged changes. Stage files with `git add` first.");
11060
+ const unstaged = getUnstagedFiles();
11061
+ if (unstaged.length > 0) {
11062
+ log.error("No staged changes. Use `qc -a` to stage tracked files, or `git add` manually.");
11063
+ } else {
11064
+ log.error("No changes to commit.");
11065
+ }
11066
+ process.exit(1);
9191
11067
  }
9192
- const local = getLocalProviderConfig();
9193
- if (!local) {
9194
- throw new Error(
9195
- "No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
9196
- );
11068
+ const apiKey = apiKeyFlag ?? getApiKey();
11069
+ if (!apiKey) {
11070
+ log.error("Not authenticated. Run `qc login` first.");
11071
+ process.exit(1);
9197
11072
  }
9198
- const config2 = getConfig();
9199
- const excludes = config2.excludes ?? [];
11073
+ const model = modelFlag ?? config2.model;
9200
11074
  const diff = getStagedDiff(excludes);
9201
11075
  const changes = getStagedFiles();
9202
- const model = modelFlag ?? local.model;
9203
- let rules = config2.rules ?? {};
11076
+ let processedDiff = diff;
11077
+ if (!args.noSmartDiff) {
11078
+ const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
11079
+ processedDiff = smartResult.processedDiff;
11080
+ if (smartResult.summarized.length > 0) {
11081
+ log.step(
11082
+ `smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
11083
+ );
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
+ }
11090
+ }
11091
+ const commitlintRules = await detectCommitlintRules();
11092
+ let rules = { ...commitlintRules, ...config2.rules ?? {} };
9204
11093
  const workspace = detectWorkspace();
11094
+ let monorepoScopes;
9205
11095
  if (workspace) {
9206
11096
  const stagedFiles = changes.trim().split("\n").filter(Boolean);
9207
11097
  const scope = autoDetectScope(stagedFiles, workspace);
9208
11098
  if (scope) {
9209
- const scopes = scope.split(",").map((s) => s.trim());
9210
- rules = { ...rules, scopes };
11099
+ monorepoScopes = scope.split(",").map((s) => s.trim());
11100
+ rules = { ...rules, scopes: monorepoScopes };
9211
11101
  }
9212
11102
  }
9213
- const userContent = buildUserPrompt(changes, diff, rules);
9214
- const { url, body, headers } = buildRequest(
9215
- local.provider,
9216
- local.baseUrl,
9217
- userContent,
9218
- diff,
9219
- changes,
9220
- model,
9221
- local.apiKey,
9222
- rules
9223
- );
9224
- if (!url || url.includes("YOUR-WORKER")) {
9225
- throw new Error(
9226
- "Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
9227
- );
9228
- }
9229
- const res = await fetch(url, {
9230
- method: "POST",
9231
- headers,
9232
- body: JSON.stringify(body)
9233
- });
9234
- if (!res.ok) {
9235
- const text = await res.text();
9236
- throw new Error(`Provider error (${res.status}): ${text}`);
11103
+ const client = new ApiClient({ apiKey });
11104
+ try {
11105
+ const teamRules = await client.getTeamRules();
11106
+ if (teamRules && Object.keys(teamRules).length > 0) {
11107
+ log.step("using team rules from org");
11108
+ rules = { ...rules, ...teamRules };
11109
+ if (monorepoScopes && teamRules.scopes && teamRules.scopes.length > 0) {
11110
+ const allowed = new Set(teamRules.scopes);
11111
+ const intersected = monorepoScopes.filter((s) => allowed.has(s));
11112
+ if (intersected.length > 0) rules = { ...rules, scopes: intersected };
11113
+ }
11114
+ }
11115
+ } catch {
9237
11116
  }
9238
- const data = await res.json();
9239
- let message = parseResponse(local.provider, data);
9240
- message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
9241
- if (!message) {
9242
- throw new Error("Failed to generate commit message.");
11117
+ rules = applyCliTypeScopeToRules(rules, args.type, args.scope);
11118
+ const recentCommits = args.noContext ? void 0 : getRecentBranchCommits(5);
11119
+ const generationHints = generationHintsFromArgs(args.split, args.forceBody);
11120
+ const skipInteractive = silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
11121
+ const skipConfirm = args.dryRun || messageOnly || silent || args.quiet || shouldSkipTTYInteraction(args.hookMode);
11122
+ const modelDisplay = model ?? "default";
11123
+ const spinner = ui2.spinner(`generating commit (${modelDisplay})...`);
11124
+ if (!silent) spinner.start();
11125
+ const t0 = Date.now();
11126
+ let generatedMessage;
11127
+ let diagnostics;
11128
+ try {
11129
+ ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11130
+ processedDiff,
11131
+ changes,
11132
+ rules,
11133
+ model,
11134
+ recentCommits,
11135
+ generationHints
11136
+ ));
11137
+ } finally {
11138
+ spinner.stop();
11139
+ }
11140
+ const roundTripMs = Date.now() - t0;
11141
+ logVerboseDiagnostics((msg) => log.dim(msg), args.verbose, args.quiet, diagnostics, roundTripMs);
11142
+ let message = generatedMessage;
11143
+ if (args.interactive) {
11144
+ if (shouldSkipTTYInteraction(args.hookMode)) {
11145
+ if (!silent) log.dim("(--interactive ignored: not running in a TTY)");
11146
+ } else {
11147
+ const refineResult = await interactiveRefineMessage(message, { skip: skipInteractive });
11148
+ if (refineResult.action === "abort") {
11149
+ return;
11150
+ }
11151
+ message = refineResult.message;
11152
+ }
9243
11153
  }
9244
11154
  if (messageOnly) {
9245
11155
  console.log(message);
9246
11156
  return;
9247
11157
  }
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
+ });
11174
+ if (args.dryRun) {
11175
+ return;
11176
+ }
11177
+ if (args.confirm) {
11178
+ const confirmResult = await confirmCommit("Proceed with commit? [y/N]: ", { skip: skipConfirm });
11179
+ if (confirmResult.action === "abort") {
11180
+ return;
11181
+ }
11182
+ }
9248
11183
  gitCommit(message);
11184
+ const hash = getCommitHash();
11185
+ const branch = getCurrentBranch();
11186
+ log.step(`[${branch} ${hash}] committed`);
9249
11187
  if (push) {
11188
+ log.step(`pushing to origin/${branch}...`);
9250
11189
  gitPush();
11190
+ const stats = getPushStats();
11191
+ if (stats) {
11192
+ log.success(`pushed ${stats.commits} commit(s) \xB7 ${stats.stat}`);
11193
+ } else {
11194
+ log.success("pushed");
11195
+ }
9251
11196
  }
9252
11197
  }
9253
- var import_fs10, import_path10, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
9254
- var init_local = __esm({
9255
- "src/local.ts"() {
11198
+ var init_commit = __esm({
11199
+ "src/commands/commit.ts"() {
9256
11200
  "use strict";
9257
- import_fs10 = require("fs");
9258
- import_path10 = require("path");
9259
- import_os4 = require("os");
9260
11201
  init_config();
9261
- init_dist();
11202
+ init_api();
11203
+ init_commitlint();
9262
11204
  init_git();
9263
11205
  init_monorepo();
9264
- CONFIG_PATH2 = (0, import_path10.join)((0, import_os4.homedir)(), CONFIG_DIR);
9265
- PROVIDER_URLS = {
9266
- ollama: "http://localhost:11434",
9267
- lmstudio: "http://localhost:1234/v1",
9268
- openrouter: "https://openrouter.ai/api/v1",
9269
- custom: "",
9270
- cloudflare: ""
9271
- };
9272
- DEFAULT_MODELS = {
9273
- ollama: "codellama",
9274
- lmstudio: "default",
9275
- openrouter: "google/gemini-flash-1.5-8b",
9276
- custom: "",
9277
- cloudflare: "@cf/qwen/qwen2.5-coder-32b-instruct"
9278
- };
11206
+ init_ui();
11207
+ init_smart_diff();
11208
+ init_commit_helpers();
11209
+ init_branch_guard();
9279
11210
  }
9280
11211
  });
9281
11212
 
9282
11213
  // src/index.ts
11214
+ var index_exports = {};
11215
+ __export(index_exports, {
11216
+ parseArgs: () => parseArgs
11217
+ });
11218
+ module.exports = __toCommonJS(index_exports);
9283
11219
  init_config();
9284
- init_api();
9285
- init_commitlint();
9286
- init_git();
9287
- init_monorepo();
9288
11220
  var HELP = `Quikcommit - AI-powered conventional commit messages
9289
11221
 
9290
11222
  Usage:
9291
11223
  qc Generate commit message and commit (default)
9292
- qc --message-only Generate message only, print to stdout
9293
- qc --push Commit and push to origin
9294
11224
  qc pr Generate PR description from branch commits
9295
11225
  qc changelog Generate changelog from commits since last tag
9296
11226
  qc changeset Automate pnpm changeset with AI
9297
- qc init Install prepare-commit-msg hook for auto-generation
11227
+ qc branch Generate branch name + create branch (use --message for description)
11228
+ qc init Install prepare-commit-msg hook
9298
11229
  qc login Sign in via browser
9299
11230
  qc logout Clear local credentials
9300
11231
  qc status Show auth, plan, usage
9301
11232
  qc team Team management (info, rules, invite)
11233
+ qc config Show/set config
11234
+
11235
+ Flags:
11236
+ -p, --push Commit and push
11237
+ -a, --all Stage all tracked changes first
11238
+ -m, --message-only Print message only (stdout, no commit)
11239
+ -v, --verbose Show diagnostics (model, token estimates, rules) + API round-trip ms on stderr
11240
+ -q, --quiet Minimal output
11241
+ -n, --dry-run Show message without committing
11242
+ -i, --interactive Interactive refinement mode
11243
+ -s, --split Multi-commit split mode
11244
+ -b, --body Force include body
11245
+ -l, --local Use local provider
11246
+ -c, --confirm Ask before committing
11247
+ -t, --type <type> Force commit type
11248
+ -S, --scope <scope> Force scope
11249
+ -e, --exclude <pat> Exclude files from diff (repeatable)
11250
+
11251
+ --no-context Skip commit history context
11252
+ --no-smart-diff Skip smart diff preprocessing
11253
+ --no-color Disable colors
11254
+ --model <id> Use specific model
11255
+ --base <branch> Base branch for pr/changeset (default: main)
11256
+ --create Create PR with gh CLI (qc pr --create)
11257
+ --from <ref> Start ref for changelog / base ref for qc branch
11258
+ --to <ref> End ref for changelog
11259
+ --write Write changelog to CHANGELOG.md
11260
+ --hook-mode Silent mode for git hooks
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
+
11273
+ Compose short flags: qc -ap (stage all + push), qc -apv (+ verbose)
9302
11274
 
9303
- Options:
9304
- -h, --help Show this help
9305
- -a, --all Stage all tracked changes before generating
9306
- -m, --message-only Generate message only
9307
- -p, --push Commit and push after generating
9308
- --api-key <key> Use this API key (overrides credentials file)
9309
- --base <branch> Base branch for qc pr, qc changeset (default: main)
9310
- --create Create PR with gh CLI after qc pr
9311
- --from <ref> Start ref for qc changelog (default: latest tag)
9312
- --to <ref> End ref for qc changelog (default: HEAD)
9313
- --write Prepend changelog to CHANGELOG.md
9314
- --version <ver> Version label for changelog header (default: derived from --to or "<from>-next")
9315
- --uninstall Remove Quikcommit hook (qc init --uninstall)
9316
- --model <id> Use specific model (e.g. qwen25-coder-32b, llama-3.3-70b)
9317
-
9318
- Commands:
9319
- qc config Show current config
9320
- qc config set <k> <v> Set config (model, api_url)
9321
- qc config reset Reset to defaults
9322
- qc upgrade Open billing page in browser
11275
+ Examples:
11276
+ qc # generate and commit
11277
+ qc -p # commit and push
11278
+ qc -ap # stage all, commit, push
11279
+ qc -m | pbcopy # copy message to clipboard
11280
+ qc -n # preview without committing
11281
+ qc -e "*.lock" # exclude lock files
11282
+ qc -t fix -S auth # force type and scope
9323
11283
  `;
11284
+ var SHORT_FLAGS = {
11285
+ p: "push",
11286
+ a: "all",
11287
+ m: "messageOnly",
11288
+ v: "verbose",
11289
+ q: "quiet",
11290
+ n: "dryRun",
11291
+ i: "interactive",
11292
+ s: "split",
11293
+ b: "forceBody",
11294
+ l: "local",
11295
+ c: "confirm"
11296
+ };
11297
+ var SHORT_FLAGS_WITH_VALUE = {
11298
+ t: "type",
11299
+ S: "scope",
11300
+ e: "exclude"
11301
+ };
9324
11302
  function parseArgs(args) {
9325
- let command = "commit";
9326
- let all = false;
9327
- let messageOnly = false;
9328
- let push = false;
9329
- let apiKey;
9330
- let model;
9331
- let local = false;
9332
- let base;
9333
- let create = false;
9334
- let from;
9335
- let to;
9336
- let write = false;
9337
- let version;
9338
- let uninstall = false;
9339
- let hookMode = false;
11303
+ const result = {
11304
+ command: "commit",
11305
+ all: false,
11306
+ messageOnly: false,
11307
+ push: false,
11308
+ verbose: false,
11309
+ quiet: false,
11310
+ dryRun: false,
11311
+ interactive: false,
11312
+ split: false,
11313
+ forceBody: false,
11314
+ confirm: false,
11315
+ noContext: false,
11316
+ noSmartDiff: false,
11317
+ local: false,
11318
+ exclude: [],
11319
+ positionals: []
11320
+ };
11321
+ let subcommandSeen = false;
9340
11322
  for (let i = 0; i < args.length; i++) {
9341
11323
  const arg = args[i];
9342
- if (arg === "-h" || arg === "--help") {
9343
- command = "help";
9344
- } else if (arg === "-a" || arg === "--all") {
9345
- all = true;
9346
- } else if (arg === "-m" || arg === "--message-only") {
9347
- messageOnly = true;
9348
- } else if (arg === "-p" || arg === "--push") {
9349
- push = true;
11324
+ if (arg === void 0) continue;
11325
+ if (arg.startsWith("-") && !arg.startsWith("--") && arg.length > 2) {
11326
+ const chars = [...arg.slice(1)];
11327
+ for (let j = 0; j < chars.length; j++) {
11328
+ const ch = chars[j];
11329
+ if (!ch) continue;
11330
+ if (SHORT_FLAGS[ch]) {
11331
+ const key = SHORT_FLAGS[ch];
11332
+ result[key] = true;
11333
+ } else if (SHORT_FLAGS_WITH_VALUE[ch]) {
11334
+ if (j < chars.length - 1) {
11335
+ throw new Error(`Flag -${ch} requires a value and must be last in a composed group`);
11336
+ }
11337
+ const val = args[++i];
11338
+ if (!val || val.startsWith("-") && val.length > 1) throw new Error(`Flag -${ch} requires a value`);
11339
+ const key = SHORT_FLAGS_WITH_VALUE[ch];
11340
+ if (key === "exclude") {
11341
+ result.exclude.push(val);
11342
+ } else {
11343
+ result[key] = val;
11344
+ }
11345
+ } else if (ch === "h") {
11346
+ result.command = "help";
11347
+ } else {
11348
+ throw new Error(`Unknown flag: -${ch}`);
11349
+ }
11350
+ }
11351
+ continue;
11352
+ }
11353
+ if (arg.length === 2 && arg.startsWith("-") && !arg.startsWith("--")) {
11354
+ const ch = arg[1];
11355
+ if (!ch) continue;
11356
+ if (SHORT_FLAGS[ch]) {
11357
+ result[SHORT_FLAGS[ch]] = true;
11358
+ continue;
11359
+ }
11360
+ if (SHORT_FLAGS_WITH_VALUE[ch]) {
11361
+ const val = args[++i];
11362
+ if (!val || val.startsWith("-") && val.length > 1) {
11363
+ throw new Error(`Flag -${ch} requires a value`);
11364
+ }
11365
+ const key = SHORT_FLAGS_WITH_VALUE[ch];
11366
+ if (key === "exclude") {
11367
+ result.exclude.push(val);
11368
+ } else {
11369
+ result[key] = val;
11370
+ }
11371
+ continue;
11372
+ }
11373
+ if (ch === "h") {
11374
+ result.command = "help";
11375
+ continue;
11376
+ }
11377
+ throw new Error(`Unknown flag: -${ch}`);
11378
+ }
11379
+ if (arg === "--help") {
11380
+ result.command = "help";
11381
+ } else if (arg === "--all") {
11382
+ result.all = true;
11383
+ } else if (arg === "--allow-protected") {
11384
+ result.allowProtected = true;
11385
+ } else if (arg === "--auto-branch") {
11386
+ result.autoBranch = true;
11387
+ } else if (arg === "--message-only") {
11388
+ result.messageOnly = true;
11389
+ } else if (arg === "--message" && i + 1 < args.length) {
11390
+ result.message = args[++i];
11391
+ } else if (arg === "--push") {
11392
+ result.push = true;
11393
+ } else if (arg === "--rescue") {
11394
+ result.rescue = true;
11395
+ } else if (arg === "--verbose") {
11396
+ result.verbose = true;
11397
+ } else if (arg === "--quiet") {
11398
+ result.quiet = true;
11399
+ } else if (arg === "--dry-run") {
11400
+ result.dryRun = true;
11401
+ } else if (arg === "--interactive") {
11402
+ result.interactive = true;
11403
+ } else if (arg === "--split") {
11404
+ result.split = true;
11405
+ } else if (arg === "--body") {
11406
+ result.forceBody = true;
11407
+ } else if (arg === "--confirm") {
11408
+ result.confirm = true;
11409
+ } else if (arg === "--no-confirm") {
11410
+ result.confirm = false;
11411
+ } else if (arg === "--no-context") {
11412
+ result.noContext = true;
11413
+ } else if (arg === "--no-smart-diff") {
11414
+ result.noSmartDiff = true;
11415
+ } else if (arg === "--no-switch") {
11416
+ result.noSwitch = true;
11417
+ } else if (arg === "--local" || arg === "--use-ollama" || arg === "--use-lmstudio" || arg === "--use-openrouter" || arg === "--use-cloudflare") {
11418
+ result.local = true;
11419
+ if (arg === "--use-ollama") {
11420
+ result.setProvider = "ollama";
11421
+ } else if (arg === "--use-lmstudio") {
11422
+ result.setProvider = "lmstudio";
11423
+ } else if (arg === "--use-openrouter") {
11424
+ result.setProvider = "openrouter";
11425
+ } else if (arg === "--use-cloudflare") {
11426
+ result.setProvider = "cloudflare";
11427
+ }
9350
11428
  } else if (arg === "--api-key" && i + 1 < args.length) {
9351
- apiKey = args[++i];
11429
+ result.apiKey = args[++i];
9352
11430
  } else if (arg === "--base" && i + 1 < args.length) {
9353
- base = args[++i];
11431
+ result.base = args[++i];
9354
11432
  } else if (arg === "--create") {
9355
- create = true;
11433
+ result.create = true;
9356
11434
  } else if (arg === "--from" && i + 1 < args.length) {
9357
- from = args[++i];
11435
+ result.from = args[++i];
11436
+ } else if (arg === "--from-commits") {
11437
+ result.fromCommits = true;
9358
11438
  } else if (arg === "--to" && i + 1 < args.length) {
9359
- to = args[++i];
11439
+ result.to = args[++i];
9360
11440
  } else if (arg === "--write") {
9361
- write = true;
11441
+ result.write = true;
9362
11442
  } else if (arg === "--version" && i + 1 < args.length) {
9363
- version = args[++i];
11443
+ result.version = args[++i];
9364
11444
  } else if (arg === "--uninstall") {
9365
- uninstall = true;
11445
+ result.uninstall = true;
9366
11446
  } else if (arg === "--hook-mode") {
9367
- hookMode = true;
11447
+ result.hookMode = true;
11448
+ } else if (arg === "--model" && i + 1 < args.length) {
11449
+ result.model = args[++i];
11450
+ } else if (arg === "--type" && i + 1 < args.length) {
11451
+ result.type = args[++i];
11452
+ } else if (arg === "--scope" && i + 1 < args.length) {
11453
+ result.scope = args[++i];
11454
+ } else if (arg === "--exclude" && i + 1 < args.length) {
11455
+ const ex = args[++i];
11456
+ if (ex) result.exclude.push(ex);
11457
+ } else if (arg === "--no-color") {
9368
11458
  } else if (arg === "login") {
9369
- command = "login";
11459
+ result.command = "login";
11460
+ subcommandSeen = true;
9370
11461
  } else if (arg === "logout") {
9371
- command = "logout";
11462
+ result.command = "logout";
11463
+ subcommandSeen = true;
9372
11464
  } else if (arg === "status") {
9373
- command = "status";
11465
+ result.command = "status";
11466
+ subcommandSeen = true;
9374
11467
  } else if (arg === "pr") {
9375
- command = "pr";
11468
+ result.command = "pr";
11469
+ subcommandSeen = true;
9376
11470
  } else if (arg === "changelog") {
9377
- command = "changelog";
11471
+ result.command = "changelog";
11472
+ subcommandSeen = true;
11473
+ } else if (arg === "branch") {
11474
+ result.command = "branch";
11475
+ subcommandSeen = true;
9378
11476
  } else if (arg === "init") {
9379
- command = "init";
11477
+ result.command = "init";
11478
+ subcommandSeen = true;
9380
11479
  } else if (arg === "team") {
9381
- command = "team";
11480
+ result.command = "team";
11481
+ subcommandSeen = true;
9382
11482
  } else if (arg === "config") {
9383
- command = "config";
11483
+ result.command = "config";
11484
+ subcommandSeen = true;
9384
11485
  } else if (arg === "upgrade") {
9385
- command = "upgrade";
11486
+ result.command = "upgrade";
11487
+ subcommandSeen = true;
9386
11488
  } else if (arg === "changeset") {
9387
- command = "changeset";
9388
- } else if (arg === "--model" && i + 1 < args.length) {
9389
- model = args[++i];
9390
- } else if (arg === "--local" || arg === "--use-ollama" || arg === "--use-lmstudio" || arg === "--use-openrouter" || arg === "--use-cloudflare") {
9391
- local = true;
9392
- if (arg === "--use-ollama") {
9393
- saveConfig({ ...getConfig(), provider: "ollama", apiUrl: "http://localhost:11434", model: "codellama" });
9394
- } else if (arg === "--use-lmstudio") {
9395
- saveConfig({ ...getConfig(), provider: "lmstudio", apiUrl: "http://localhost:1234/v1", model: "default" });
9396
- } else if (arg === "--use-openrouter") {
9397
- saveConfig({ ...getConfig(), provider: "openrouter", apiUrl: "https://openrouter.ai/api/v1", model: "google/gemini-flash-1.5-8b" });
9398
- } else if (arg === "--use-cloudflare") {
9399
- saveConfig({
9400
- ...getConfig(),
9401
- provider: "cloudflare",
9402
- apiUrl: "https://YOUR-WORKER.workers.dev",
9403
- model: "@cf/qwen/qwen2.5-coder-32b-instruct"
9404
- });
9405
- console.error(
9406
- "[qc] Cloudflare provider set. Run: qc config set api_url https://your-worker.workers.dev"
9407
- );
9408
- }
9409
- }
9410
- }
9411
- return { command, all, messageOnly, push, apiKey, base, create, from, to, write, version, uninstall, hookMode, model, local };
9412
- }
9413
- async function runCommit(messageOnly, push, apiKeyFlag, hookMode = false, modelFlag, stageAll_) {
9414
- const log = hookMode ? () => {
9415
- } : (msg) => console.error(msg);
9416
- if (!isGitRepo()) {
9417
- log("Error: Not a git repository.");
9418
- process.exit(1);
9419
- }
9420
- const config2 = getConfig();
9421
- if (stageAll_ || config2.autoStage) {
9422
- stageAll();
9423
- }
9424
- if (!hasStagedChanges()) {
9425
- const unstaged = getUnstagedFiles();
9426
- if (unstaged.length > 0) {
9427
- log("Error: No staged changes. Use `qc -a` to stage tracked files, or `git add` manually.");
9428
- } else {
9429
- log("Error: No changes to commit.");
9430
- }
9431
- process.exit(1);
9432
- }
9433
- const apiKey = apiKeyFlag ?? getApiKey();
9434
- if (!apiKey) {
9435
- log("Error: Not authenticated. Run `qc login` first.");
9436
- process.exit(1);
9437
- }
9438
- const model = modelFlag ?? config2.model;
9439
- const excludes = config2.excludes ?? [];
9440
- const diff = getStagedDiff(excludes);
9441
- const changes = getStagedFiles();
9442
- const commitlintRules = await detectCommitlintRules();
9443
- let rules = { ...commitlintRules, ...config2.rules ?? {} };
9444
- const workspace = detectWorkspace();
9445
- let monorepoScopes;
9446
- if (workspace) {
9447
- const stagedFiles = changes.trim().split("\n").filter(Boolean);
9448
- const scope = autoDetectScope(stagedFiles, workspace);
9449
- if (scope) {
9450
- monorepoScopes = scope.split(",").map((s) => s.trim());
9451
- rules = { ...rules, scopes: monorepoScopes };
11489
+ result.command = "changeset";
11490
+ subcommandSeen = true;
11491
+ } else if (subcommandSeen && !arg.startsWith("-")) {
11492
+ result.positionals.push(arg);
9452
11493
  }
9453
11494
  }
9454
- const client = new ApiClient({ apiKey });
9455
- try {
9456
- const teamRules = await client.getTeamRules();
9457
- if (teamRules && Object.keys(teamRules).length > 0) {
9458
- log("[qc] Using team rules from org");
9459
- rules = { ...rules, ...teamRules };
9460
- if (monorepoScopes && teamRules.scopes && teamRules.scopes.length > 0) {
9461
- const allowed = new Set(teamRules.scopes);
9462
- const intersected = monorepoScopes.filter((s) => allowed.has(s));
9463
- if (intersected.length > 0) rules = { ...rules, scopes: intersected };
9464
- }
9465
- }
9466
- } catch {
11495
+ if (result.messageOnly && result.push) {
11496
+ throw new Error("Cannot combine --message-only (-m) with --push (-p)");
9467
11497
  }
9468
- const { message } = await client.generateCommit(diff, changes, rules, model);
9469
- if (messageOnly) {
9470
- console.log(message);
9471
- return;
11498
+ if (result.quiet && result.verbose) {
11499
+ throw new Error("Cannot combine --quiet (-q) with --verbose (-v)");
9472
11500
  }
9473
- gitCommit(message);
9474
- if (push) {
9475
- gitPush();
11501
+ if (result.dryRun && result.push) {
11502
+ throw new Error("Cannot combine --dry-run (-n) with --push (-p). Pick one.");
9476
11503
  }
11504
+ return result;
9477
11505
  }
9478
11506
  async function main() {
9479
11507
  const argv = process.argv.slice(2);
9480
- const values = parseArgs(argv);
9481
- const { command, messageOnly, push, apiKey } = values;
11508
+ let values;
11509
+ try {
11510
+ values = parseArgs(argv);
11511
+ } catch (err) {
11512
+ console.error(err instanceof Error ? err.message : String(err));
11513
+ process.exit(1);
11514
+ }
11515
+ const { command, apiKey } = values;
9482
11516
  if (command === "help") {
9483
11517
  console.log(HELP);
9484
11518
  return;
9485
11519
  }
11520
+ if (values.setProvider) {
11521
+ switch (values.setProvider) {
11522
+ case "ollama":
11523
+ saveConfig({ ...getConfig(), provider: "ollama", apiUrl: "http://localhost:11434", model: "codellama" });
11524
+ break;
11525
+ case "lmstudio":
11526
+ saveConfig({ ...getConfig(), provider: "lmstudio", apiUrl: "http://localhost:1234/v1", model: "default" });
11527
+ break;
11528
+ case "openrouter":
11529
+ saveConfig({
11530
+ ...getConfig(),
11531
+ provider: "openrouter",
11532
+ apiUrl: "https://openrouter.ai/api/v1",
11533
+ model: "google/gemini-flash-1.5-8b"
11534
+ });
11535
+ break;
11536
+ case "cloudflare":
11537
+ saveConfig({
11538
+ ...getConfig(),
11539
+ provider: "cloudflare",
11540
+ apiUrl: "https://YOUR-WORKER.workers.dev",
11541
+ model: "@cf/qwen/qwen2.5-coder-32b-instruct"
11542
+ });
11543
+ console.error("[qc] Cloudflare provider set. Run: qc config set api_url https://your-worker.workers.dev");
11544
+ break;
11545
+ }
11546
+ }
9486
11547
  if (command === "login") {
9487
11548
  const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
9488
11549
  await runLogin2();
@@ -9526,6 +11587,23 @@ async function main() {
9526
11587
  });
9527
11588
  return;
9528
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
+ }
9529
11607
  if (command === "init") {
9530
11608
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
9531
11609
  init2({ uninstall: values.uninstall });
@@ -9533,14 +11611,12 @@ async function main() {
9533
11611
  }
9534
11612
  if (command === "team") {
9535
11613
  const { team: team2 } = await Promise.resolve().then(() => (init_team(), team_exports));
9536
- const positionals = argv.filter((a) => !a.startsWith("-") && a !== "team");
9537
- await team2(positionals[0], positionals.slice(1));
11614
+ await team2(values.positionals[0], values.positionals.slice(1));
9538
11615
  return;
9539
11616
  }
9540
11617
  if (command === "config") {
9541
- const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
9542
- const positionals = argv.filter((a) => !a.startsWith("-") && a !== "config");
9543
- config2(positionals);
11618
+ const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
11619
+ config2(values.positionals);
9544
11620
  return;
9545
11621
  }
9546
11622
  if (command === "upgrade") {
@@ -9550,7 +11626,7 @@ async function main() {
9550
11626
  }
9551
11627
  if (values.local) {
9552
11628
  const { runLocalCommit: runLocalCommit2 } = await Promise.resolve().then(() => (init_local(), local_exports));
9553
- await runLocalCommit2(messageOnly, push, values.model);
11629
+ await runLocalCommit2(values);
9554
11630
  return;
9555
11631
  }
9556
11632
  const apiKeyToUse = apiKey ?? getApiKey();
@@ -9558,11 +11634,12 @@ async function main() {
9558
11634
  const { getLocalProviderConfig: getLocalProviderConfig2 } = await Promise.resolve().then(() => (init_local(), local_exports));
9559
11635
  if (getLocalProviderConfig2()) {
9560
11636
  const { runLocalCommit: runLocalCommit2 } = await Promise.resolve().then(() => (init_local(), local_exports));
9561
- await runLocalCommit2(messageOnly, push, values.model);
11637
+ await runLocalCommit2(values);
9562
11638
  return;
9563
11639
  }
9564
11640
  }
9565
- await runCommit(messageOnly, push, apiKey, values.hookMode, values.model, values.all);
11641
+ const { runCommit: runCommit2 } = await Promise.resolve().then(() => (init_commit(), commit_exports));
11642
+ await runCommit2(values);
9566
11643
  }
9567
11644
  main().catch((err) => {
9568
11645
  const args = process.argv.slice(2);
@@ -9572,3 +11649,7 @@ main().catch((err) => {
9572
11649
  }
9573
11650
  process.exit(1);
9574
11651
  });
11652
+ // Annotate the CommonJS export names for ESM import in node:
11653
+ 0 && (module.exports = {
11654
+ parseArgs
11655
+ });