@quikcommit/cli 12.1.0 → 13.1.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 +236 -83
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -89,20 +89,59 @@ function slugifyFilename(path) {
89
89
  const noExt = basename.replace(/\.[^.]+$/, "");
90
90
  return noExt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
91
91
  }
92
+ function isTestPath(f) {
93
+ return /\.(test|spec)\.[^.]+$/i.test(f) || /^tests?\//i.test(f);
94
+ }
92
95
  function pickBestFile(files) {
93
96
  const codeFiles = files.filter((f) => !NON_CODE_PATTERNS.some((rx) => rx.test(f)));
94
- return codeFiles[0] ?? files[0] ?? "";
97
+ const srcDirs = codeFiles.filter((f) => /^(src|lib|app|packages)\//.test(f));
98
+ const nonTestSrc = srcDirs.filter((f) => !isTestPath(f));
99
+ return nonTestSrc[0] ?? srcDirs[0] ?? codeFiles[0] ?? files[0] ?? "";
100
+ }
101
+ function inferTypeFromFiles(files) {
102
+ const isNonCode = (f) => NON_CODE_PATTERNS.some((rx) => rx.test(f));
103
+ let srcCount = 0, testCount = 0, docCount = 0, ciCount = 0;
104
+ for (const f of files) {
105
+ if (/^\.github\//i.test(f)) {
106
+ ciCount++;
107
+ } else if (isTestPath(f)) {
108
+ testCount++;
109
+ } else if (/^docs?\//i.test(f) || /\.md$/i.test(f) || /^readme/i.test(f)) {
110
+ docCount++;
111
+ } else if (!isNonCode(f)) {
112
+ srcCount++;
113
+ }
114
+ }
115
+ if (srcCount === 0 && testCount > 0 && docCount === 0 && ciCount === 0)
116
+ return "test";
117
+ if (srcCount === 0 && docCount > 0 && testCount === 0 && ciCount === 0)
118
+ return "docs";
119
+ if (srcCount === 0 && ciCount > 0 && testCount === 0 && docCount === 0)
120
+ return "ci";
121
+ if (srcCount > 10)
122
+ return "refactor";
123
+ if (srcCount > 0)
124
+ return "feat";
125
+ if (testCount >= docCount && testCount >= ciCount)
126
+ return "test";
127
+ if (docCount >= ciCount)
128
+ return "docs";
129
+ if (ciCount > 0)
130
+ return "ci";
131
+ return "chore";
95
132
  }
96
133
  function deterministicBranchName(opts) {
97
134
  const files = opts.files ?? [];
98
- const codeFiles = files.filter((f) => !NON_CODE_PATTERNS.some((rx) => rx.test(f)));
99
- const haystack = `${opts.description ?? ""} ${(codeFiles.length > 0 ? codeFiles : files).join(" ")}`;
100
135
  let type = "chore";
101
- for (const [rx, t] of TYPE_HINTS) {
102
- if (rx.test(haystack)) {
103
- type = t;
104
- break;
136
+ if (opts.description) {
137
+ for (const [rx, t] of TYPE_HINTS) {
138
+ if (rx.test(opts.description)) {
139
+ type = t;
140
+ break;
141
+ }
105
142
  }
143
+ } else if (files.length > 0) {
144
+ type = inferTypeFromFiles(files);
106
145
  }
107
146
  let slug;
108
147
  if (opts.description) {
@@ -127,16 +166,16 @@ var init_branch = __esm({
127
166
  "../shared/dist/branch.js"() {
128
167
  "use strict";
129
168
  BRANCH_NAME_RX = /^(feat|fix|refactor|perf|docs|test|chore|ci)\/[a-z0-9][a-z0-9-]{0,51}$/;
130
- PROTECTED_BRANCH_RX = /(^|[/-])(main|master|develop|trunk|release)([/-]|$)/i;
169
+ PROTECTED_BRANCH_RX = /(^|\/)(main|master|develop|trunk|release)(\/|$)/i;
131
170
  MAX_BRANCH_NAME_LENGTH = 60;
132
171
  TYPE_HINTS = [
133
- [/\btest|spec\b/i, "test"],
134
- [/\bdocs?\b|readme|\.md$/i, "docs"],
135
- [/\bperf|benchmark/i, "perf"],
172
+ [/\b(ci|workflow)\b|github\/actions/i, "ci"],
173
+ [/\b(perf|benchmark)\b/i, "perf"],
136
174
  [/\brefactor\b/i, "refactor"],
137
- [/\bci|workflow|github\/actions/i, "ci"],
138
- [/\bfix|bug|issue/i, "fix"],
139
- [/\bfeat|add|new\b/i, "feat"]
175
+ [/\b(fix|bug|issue|patch)\b/i, "fix"],
176
+ [/\b(feat|add|new)\b/i, "feat"],
177
+ [/\b(docs?)\b|\breadme\b/i, "docs"],
178
+ [/\b(test|spec)\b/i, "test"]
140
179
  ];
141
180
  NON_CODE_PATTERNS = [
142
181
  /^docs?\//i,
@@ -150,6 +189,8 @@ var init_branch = __esm({
150
189
  /\.json$/,
151
190
  /\.toml$/,
152
191
  /\/\.?config\b/i,
192
+ /\.config\.[a-z]+$/i,
193
+ // root-level config: eslint.config.mjs, vitest.config.ts
153
194
  /^\.github\//,
154
195
  /^\.vscode\//
155
196
  ];
@@ -400,7 +441,8 @@ var init_api = __esm({
400
441
  "Content-Type": "application/json",
401
442
  Authorization: `Bearer ${this.apiKey}`
402
443
  },
403
- body: JSON.stringify(body)
444
+ body: JSON.stringify(body),
445
+ signal: AbortSignal.timeout(3e4)
404
446
  });
405
447
  if (!res.ok) {
406
448
  if (res.status === 413) {
@@ -476,7 +518,8 @@ var init_api = __esm({
476
518
  "Content-Type": "application/json",
477
519
  Authorization: `Bearer ${this.apiKey}`
478
520
  },
479
- body: options?.body
521
+ body: options?.body,
522
+ signal: AbortSignal.timeout(3e4)
480
523
  });
481
524
  if (!res.ok) {
482
525
  const err = await res.json().catch(() => ({ error: res.statusText }));
@@ -7868,6 +7911,7 @@ __export(git_exports, {
7868
7911
  createBranch: () => createBranch,
7869
7912
  deleteBranch: () => deleteBranch,
7870
7913
  getAllChangedFiles: () => getAllChangedFiles,
7914
+ getAllChangedFilesWithStatus: () => getAllChangedFilesWithStatus,
7871
7915
  getBranchCommits: () => getBranchCommits,
7872
7916
  getChangedFilesSince: () => getChangedFilesSince,
7873
7917
  getCommitHash: () => getCommitHash,
@@ -7890,6 +7934,7 @@ __export(git_exports, {
7890
7934
  getStagedFiles: () => getStagedFiles,
7891
7935
  getUnstagedFiles: () => getUnstagedFiles,
7892
7936
  getUpstreamRef: () => getUpstreamRef,
7937
+ getWorkingDiffStat: () => getWorkingDiffStat,
7893
7938
  getWorkingTreeDiff: () => getWorkingTreeDiff,
7894
7939
  gitCommit: () => gitCommit,
7895
7940
  gitPush: () => gitPush,
@@ -7945,7 +7990,8 @@ function getStagedDiff(excludes = []) {
7945
7990
  }
7946
7991
  function getStagedFiles() {
7947
7992
  return (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7948
- encoding: "utf-8"
7993
+ encoding: "utf-8",
7994
+ maxBuffer: 10 * 1024 * 1024
7949
7995
  });
7950
7996
  }
7951
7997
  function getWorkingTreeDiff(excludes = []) {
@@ -7968,9 +8014,36 @@ function getAllChangedFiles() {
7968
8014
  });
7969
8015
  return output.trim().split("\n").filter(Boolean).map((line) => line.slice(3)).join("\n");
7970
8016
  }
8017
+ function getAllChangedFilesWithStatus() {
8018
+ const output = (0, import_child_process2.execFileSync)("git", ["status", "--porcelain"], {
8019
+ encoding: "utf-8"
8020
+ });
8021
+ return output.trim().split("\n").filter(Boolean).map((line) => {
8022
+ const xy = line.slice(0, 2).trim();
8023
+ const filepath = line.slice(3);
8024
+ let status = "M";
8025
+ if (xy === "??") status = "A";
8026
+ else if (xy.includes("D")) status = "D";
8027
+ else if (xy.includes("A")) status = "A";
8028
+ else if (xy.includes("R")) status = "R";
8029
+ return `${status} ${filepath}`;
8030
+ }).join("\n");
8031
+ }
8032
+ function getWorkingDiffStat(staged) {
8033
+ const args = staged ? ["diff", "--cached", "--stat=120"] : ["diff", "HEAD", "--stat=120"];
8034
+ try {
8035
+ return (0, import_child_process2.execFileSync)("git", args, {
8036
+ encoding: "utf-8",
8037
+ maxBuffer: 1024 * 1024
8038
+ }).trim();
8039
+ } catch {
8040
+ return "";
8041
+ }
8042
+ }
7971
8043
  function hasStagedChanges() {
7972
8044
  const output = (0, import_child_process2.execFileSync)("git", ["diff", "--cached", "--name-only"], {
7973
- encoding: "utf-8"
8045
+ encoding: "utf-8",
8046
+ maxBuffer: 10 * 1024 * 1024
7974
8047
  });
7975
8048
  return output.trim().length > 0;
7976
8049
  }
@@ -8839,7 +8912,8 @@ function isMinified(content) {
8839
8912
  (l) => (l.startsWith("+") || l.startsWith("-")) && !l.startsWith("+++") && !l.startsWith("---")
8840
8913
  );
8841
8914
  if (lines.length === 0) return false;
8842
- return lines.some((l) => l.length > 500);
8915
+ const longLines = lines.filter((l) => l.length > 500).length;
8916
+ return longLines > lines.length / 2;
8843
8917
  }
8844
8918
  function preprocessDiff(diff) {
8845
8919
  const files = parseDiffIntoFiles(diff);
@@ -9564,7 +9638,9 @@ function sanitizeBranchName(input) {
9564
9638
  function ensureUniqueName(name, exists) {
9565
9639
  if (!exists(name)) return name;
9566
9640
  for (let i = 2; i <= 100; i++) {
9567
- const candidate = `${name}-${i}`;
9641
+ const suffix = `-${i}`;
9642
+ const base = name.length + suffix.length > MAX_BRANCH_NAME_LENGTH ? name.slice(0, MAX_BRANCH_NAME_LENGTH - suffix.length) : name;
9643
+ const candidate = `${base}${suffix}`;
9568
9644
  if (!exists(candidate)) return candidate;
9569
9645
  }
9570
9646
  throw new Error(`Could not find a unique name for ${name} after 100 attempts`);
@@ -10835,6 +10911,8 @@ async function generateLocalBranchName(opts) {
10835
10911
  sections.push("Generate a git branch name in the format <type>/<kebab-case-slug>.");
10836
10912
  sections.push("Type must be one of: feat, fix, refactor, perf, docs, test, chore, ci.");
10837
10913
  sections.push("Slug: 2-5 words, lowercase, hyphen-separated, max 55 chars.");
10914
+ sections.push("For LARGE changesets: identify the dominant THEME (migration, refactor, feature). Name for the theme, not a single file.");
10915
+ sections.push("Look for DELETED + ADDED files as signals of migration.");
10838
10916
  sections.push("Output ONLY the branch name on a single line. No explanation.");
10839
10917
  sections.push("");
10840
10918
  if (opts.description) {
@@ -10843,9 +10921,22 @@ async function generateLocalBranchName(opts) {
10843
10921
  } else if (opts.recentCommits && opts.recentCommits.length > 0) {
10844
10922
  sections.push("RECENT COMMITS:");
10845
10923
  for (const c of opts.recentCommits) sections.push(`- ${c}`);
10846
- } else if (opts.diff) {
10847
- sections.push("DIFF:");
10848
- sections.push(opts.diff.slice(0, 3e4));
10924
+ } else {
10925
+ if (opts.changes) {
10926
+ sections.push("FILES CHANGED (PRIMARY signal \u2014 complete list):");
10927
+ sections.push(opts.changes.slice(0, 8e3));
10928
+ sections.push("");
10929
+ }
10930
+ if (opts.diffStat) {
10931
+ sections.push("CHANGE MAGNITUDE:");
10932
+ sections.push(opts.diffStat.slice(0, 6e3));
10933
+ sections.push("");
10934
+ }
10935
+ if (opts.diff) {
10936
+ const budget = opts.diffStat ? 8e3 : 3e4;
10937
+ sections.push("DIFF (supplementary, may be truncated):");
10938
+ sections.push(opts.diff.slice(0, budget));
10939
+ }
10849
10940
  }
10850
10941
  const userContent = sections.join("\n");
10851
10942
  const model = opts.model ?? local.model;
@@ -10883,6 +10974,7 @@ async function generateLocalBranchName(opts) {
10883
10974
  body = {
10884
10975
  diff: opts.diff,
10885
10976
  changes: opts.changes,
10977
+ diff_stat: opts.diffStat,
10886
10978
  recent_commits: opts.recentCommits,
10887
10979
  description: opts.description,
10888
10980
  model,
@@ -10952,6 +11044,7 @@ async function runLocalBranch(opts) {
10952
11044
  description: opts.description,
10953
11045
  diff: opts.diff,
10954
11046
  changes: opts.changes,
11047
+ diffStat: opts.diffStat,
10955
11048
  recentCommits: opts.recentCommits,
10956
11049
  model: opts.model,
10957
11050
  rules: opts.rules
@@ -11194,8 +11287,10 @@ async function runBranch(opts) {
11194
11287
  );
11195
11288
  }
11196
11289
  const rawDiff = getStagedDiff(config2.excludes ?? []);
11197
- payload.diff = preprocessDiff(rawDiff).processedDiff;
11290
+ const processed = preprocessDiff(rawDiff).processedDiff;
11291
+ payload.diff = processed.slice(0, 6e4) || void 0;
11198
11292
  payload.changes = getStagedFiles();
11293
+ payload.diff_stat = getWorkingDiffStat(true) || void 0;
11199
11294
  }
11200
11295
  const apiKey = opts.apiKey ?? getApiKey();
11201
11296
  if (!apiKey) {
@@ -11205,6 +11300,7 @@ async function runBranch(opts) {
11205
11300
  description: opts.message,
11206
11301
  diff: opts.message ? void 0 : payload.diff,
11207
11302
  changes: opts.message ? void 0 : payload.changes,
11303
+ diffStat: opts.message ? void 0 : payload.diff_stat,
11208
11304
  recentCommits: payload.recent_commits,
11209
11305
  model: opts.model,
11210
11306
  noSwitch: opts.noSwitch,
@@ -11232,6 +11328,10 @@ async function runBranch(opts) {
11232
11328
  try {
11233
11329
  const client = new ApiClient({ apiKey });
11234
11330
  result = await client.generateBranchName(payload);
11331
+ } catch {
11332
+ const filesArr = payload.changes?.split("\n").filter(Boolean) ?? [];
11333
+ result = deterministicBranchName({ files: filesArr, description: payload.description });
11334
+ log.dim("(used deterministic fallback; API generation failed)");
11235
11335
  } finally {
11236
11336
  spinner.stop();
11237
11337
  }
@@ -11647,15 +11747,21 @@ async function runBranchGuard(args, log) {
11647
11747
  return { action: "continue" };
11648
11748
  }
11649
11749
  let stagedDiff = "";
11650
- let stagedChanges = "";
11750
+ let plainFiles = "";
11751
+ let changesForAI = "";
11752
+ let diffStat = "";
11651
11753
  if (state.mode === "uncommitted") {
11652
11754
  let rawDiff = getStagedDiff(args.excludes ?? []);
11653
- stagedChanges = getStagedFiles();
11654
- if (!rawDiff.trim()) {
11755
+ plainFiles = getStagedFiles();
11756
+ changesForAI = plainFiles;
11757
+ const isStaged = !!rawDiff.trim();
11758
+ if (!isStaged) {
11655
11759
  rawDiff = getWorkingTreeDiff(args.excludes ?? []);
11656
- stagedChanges = getAllChangedFiles();
11760
+ plainFiles = getAllChangedFiles();
11761
+ changesForAI = getAllChangedFilesWithStatus();
11657
11762
  }
11658
11763
  stagedDiff = preprocessDiff(rawDiff).processedDiff;
11764
+ diffStat = getWorkingDiffStat(isStaged);
11659
11765
  }
11660
11766
  const recentCommits = state.mode === "rescue" ? getRecentBranchCommits(state.commitsAhead) : void 0;
11661
11767
  const branchRules = args.branchRules ?? (config2.branch?.generation?.types && config2.branch.generation.types.length > 0 ? { types: [...config2.branch.generation.types] } : void 0);
@@ -11685,30 +11791,33 @@ async function runBranchGuard(args, log) {
11685
11791
  if (apiKey) {
11686
11792
  const client = new ApiClient({ apiKey });
11687
11793
  try {
11794
+ const cappedDiff = stagedDiff ? stagedDiff.slice(0, 6e4) : void 0;
11688
11795
  const branchResult = await client.generateBranchName({
11689
- diff: stagedDiff || void 0,
11690
- changes: stagedChanges || void 0,
11796
+ diff: cappedDiff || void 0,
11797
+ changes: changesForAI || void 0,
11798
+ diff_stat: diffStat || void 0,
11691
11799
  recent_commits: recentCommits,
11692
11800
  model: args.model,
11693
11801
  rules: branchRules
11694
11802
  });
11695
11803
  rawName = branchResult.name;
11696
11804
  } catch {
11697
- const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
11805
+ const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: plainFiles ? plainFiles.split("\n").filter(Boolean) : [] };
11698
11806
  rawName = deterministicBranchName(fallbackInput).name;
11699
11807
  usedFallback = true;
11700
11808
  }
11701
11809
  } else {
11702
11810
  try {
11703
11811
  rawName = await generateLocalBranchNameFn({
11704
- diff: stagedDiff || void 0,
11705
- changes: stagedChanges || void 0,
11812
+ diff: stagedDiff ? stagedDiff.slice(0, 6e4) : void 0,
11813
+ changes: changesForAI || void 0,
11814
+ diffStat: diffStat || void 0,
11706
11815
  recentCommits,
11707
11816
  model: args.model,
11708
11817
  rules: branchRules
11709
11818
  });
11710
11819
  } catch {
11711
- const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: stagedChanges ? stagedChanges.split("\n").filter(Boolean) : [] };
11820
+ const fallbackInput = state.mode === "rescue" ? { files: [], description: recentCommits?.join(" ") ?? "" } : { files: plainFiles ? plainFiles.split("\n").filter(Boolean) : [] };
11712
11821
  rawName = deterministicBranchName(fallbackInput).name;
11713
11822
  usedFallback = true;
11714
11823
  }
@@ -11886,7 +11995,11 @@ async function runCommit(args) {
11886
11995
  if (intersected.length > 0) rules = { ...rules, scopes: intersected };
11887
11996
  }
11888
11997
  }
11889
- } catch {
11998
+ } catch (err) {
11999
+ const msg = err instanceof Error ? err.message : String(err);
12000
+ if (msg && !/not found|404|403/i.test(msg)) {
12001
+ log.dim("\u26A0 could not fetch team rules: " + msg.slice(0, 80));
12002
+ }
11890
12003
  }
11891
12004
  rules = applyCliTypeScopeToRules(rules, args.type, args.scope);
11892
12005
  const recentCommits = args.noContext ? void 0 : getRecentBranchCommits(5);
@@ -11896,72 +12009,86 @@ async function runCommit(args) {
11896
12009
  const modelDisplay = model ?? "default";
11897
12010
  const uiCtx = buildUIContext(ui2, config2, args);
11898
12011
  const boxStyle = args.boxStyleOverride ?? config2.ui?.box?.style ?? "gradient";
12012
+ const t0 = Date.now();
12013
+ let generatedMessage;
12014
+ let diagnostics;
12015
+ async function generateViaChunks() {
12016
+ const chunks = splitDiffIntoChunks(processedDiff);
12017
+ if (chunks.length === 0) {
12018
+ log.error("No parseable diff content to analyze.");
12019
+ process.exit(1);
12020
+ }
12021
+ if (!silent) log.step(`large diff \u2014 analyzing ${chunks.length} chunk(s) in parallel...`);
12022
+ const results = await Promise.allSettled(
12023
+ chunks.map(
12024
+ (chunk) => client.summarizeChunk(chunk.diff, chunk.files.filter(Boolean).join("\n") || "unknown", model)
12025
+ )
12026
+ );
12027
+ const summaries = [];
12028
+ for (const r of results) {
12029
+ if (r.status === "fulfilled" && r.value) {
12030
+ summaries.push(r.value);
12031
+ }
12032
+ }
12033
+ if (summaries.length === 0) {
12034
+ log.error("All chunk summaries failed. Check your connection and try again.");
12035
+ process.exit(1);
12036
+ }
12037
+ if (results.some((r) => r.status === "rejected") && !silent) {
12038
+ const failed = results.filter((r) => r.status === "rejected").length;
12039
+ log.step(`${failed}/${results.length} chunk(s) failed \u2014 continuing with partial summaries`);
12040
+ }
12041
+ const combinedSummary = summaries.join("\n\n");
12042
+ const finalSpinner = createStageSpinner({
12043
+ stage: "aiGenerate",
12044
+ message: `generating commit from ${chunks.length} summaries (${modelDisplay})...`,
12045
+ ...uiCtx
12046
+ });
12047
+ if (!silent) finalSpinner.start();
12048
+ try {
12049
+ const result = await client.generateCommit(
12050
+ combinedSummary,
12051
+ changes,
12052
+ rules,
12053
+ model,
12054
+ recentCommits,
12055
+ generationHints
12056
+ );
12057
+ return result;
12058
+ } finally {
12059
+ finalSpinner.stop();
12060
+ }
12061
+ }
11899
12062
  const spinner = createStageSpinner({
11900
12063
  stage: "aiGenerate",
11901
12064
  message: needsChunking ? `analyzing ${changes.trim().split("\n").length} files in chunks (${modelDisplay})...` : `generating commit (${modelDisplay})...`,
11902
12065
  ...uiCtx
11903
12066
  });
11904
12067
  if (!silent) spinner.start();
11905
- const t0 = Date.now();
11906
- let generatedMessage;
11907
- let diagnostics;
11908
12068
  try {
11909
12069
  if (needsChunking) {
11910
- const chunks = splitDiffIntoChunks(processedDiff);
11911
- if (chunks.length === 0) {
11912
- spinner.stop();
11913
- log.error("No parseable diff content to analyze.");
11914
- process.exit(1);
11915
- }
11916
12070
  spinner.stop();
11917
- if (!silent) log.step(`large diff \u2014 analyzing ${chunks.length} chunk(s) in parallel...`);
11918
- const results = await Promise.allSettled(
11919
- chunks.map(
11920
- (chunk) => client.summarizeChunk(chunk.diff, chunk.files.filter(Boolean).join("\n") || "unknown", model)
11921
- )
11922
- );
11923
- const summaries = [];
11924
- for (const r of results) {
11925
- if (r.status === "fulfilled" && r.value) {
11926
- summaries.push(r.value);
11927
- }
11928
- }
11929
- if (summaries.length === 0) {
11930
- log.error("All chunk summaries failed. Check your connection and try again.");
11931
- process.exit(1);
11932
- }
11933
- if (results.some((r) => r.status === "rejected") && !silent) {
11934
- const failed = results.filter((r) => r.status === "rejected").length;
11935
- log.step(`${failed}/${results.length} chunk(s) failed \u2014 continuing with partial summaries`);
11936
- }
11937
- const combinedSummary = summaries.join("\n\n");
11938
- const finalSpinner = createStageSpinner({
11939
- stage: "aiGenerate",
11940
- message: `generating commit from ${chunks.length} summaries (${modelDisplay})...`,
11941
- ...uiCtx
11942
- });
11943
- if (!silent) finalSpinner.start();
12071
+ ({ message: generatedMessage, diagnostics } = await generateViaChunks());
12072
+ } else {
11944
12073
  try {
11945
12074
  ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11946
- combinedSummary,
12075
+ processedDiff,
11947
12076
  changes,
11948
12077
  rules,
11949
12078
  model,
11950
12079
  recentCommits,
11951
12080
  generationHints
11952
12081
  ));
11953
- } finally {
11954
- finalSpinner.stop();
12082
+ } catch (err) {
12083
+ const is413 = err instanceof Error && /too large/i.test(err.message);
12084
+ if (is413) {
12085
+ spinner.stop();
12086
+ if (!silent) log.step("diff too large for single request \u2014 switching to chunk mode...");
12087
+ ({ message: generatedMessage, diagnostics } = await generateViaChunks());
12088
+ } else {
12089
+ throw err;
12090
+ }
11955
12091
  }
11956
- } else {
11957
- ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11958
- processedDiff,
11959
- changes,
11960
- rules,
11961
- model,
11962
- recentCommits,
11963
- generationHints
11964
- ));
11965
12092
  }
11966
12093
  } finally {
11967
12094
  spinner.stop();
@@ -12312,6 +12439,8 @@ function parseArgs(args) {
12312
12439
  result.messageOnly = true;
12313
12440
  } else if (arg === "--message" && i + 1 < args.length) {
12314
12441
  result.message = args[++i];
12442
+ } else if (arg === "--message") {
12443
+ throw new Error("Flag --message requires a value");
12315
12444
  } else if (arg === "--push") {
12316
12445
  result.push = true;
12317
12446
  } else if (arg === "--rescue") {
@@ -12351,33 +12480,51 @@ function parseArgs(args) {
12351
12480
  }
12352
12481
  } else if (arg === "--api-key" && i + 1 < args.length) {
12353
12482
  result.apiKey = args[++i];
12483
+ } else if (arg === "--api-key") {
12484
+ throw new Error("Flag --api-key requires a value");
12354
12485
  } else if (arg === "--base" && i + 1 < args.length) {
12355
12486
  result.base = args[++i];
12487
+ } else if (arg === "--base") {
12488
+ throw new Error("Flag --base requires a value");
12356
12489
  } else if (arg === "--create") {
12357
12490
  result.create = true;
12358
12491
  } else if (arg === "--from" && i + 1 < args.length) {
12359
12492
  result.from = args[++i];
12493
+ } else if (arg === "--from") {
12494
+ throw new Error("Flag --from requires a value");
12360
12495
  } else if (arg === "--from-commits") {
12361
12496
  result.fromCommits = true;
12362
12497
  } else if (arg === "--to" && i + 1 < args.length) {
12363
12498
  result.to = args[++i];
12499
+ } else if (arg === "--to") {
12500
+ throw new Error("Flag --to requires a value");
12364
12501
  } else if (arg === "--write") {
12365
12502
  result.write = true;
12366
12503
  } else if (arg === "--version" && i + 1 < args.length) {
12367
12504
  result.version = args[++i];
12505
+ } else if (arg === "--version") {
12506
+ throw new Error("Flag --version requires a value");
12368
12507
  } else if (arg === "--uninstall") {
12369
12508
  result.uninstall = true;
12370
12509
  } else if (arg === "--hook-mode") {
12371
12510
  result.hookMode = true;
12372
12511
  } else if (arg === "--model" && i + 1 < args.length) {
12373
12512
  result.model = args[++i];
12513
+ } else if (arg === "--model") {
12514
+ throw new Error("Flag --model requires a value");
12374
12515
  } else if (arg === "--type" && i + 1 < args.length) {
12375
12516
  result.type = args[++i];
12517
+ } else if (arg === "--type") {
12518
+ throw new Error("Flag --type requires a value");
12376
12519
  } else if (arg === "--scope" && i + 1 < args.length) {
12377
12520
  result.scope = args[++i];
12521
+ } else if (arg === "--scope") {
12522
+ throw new Error("Flag --scope requires a value");
12378
12523
  } else if (arg === "--exclude" && i + 1 < args.length) {
12379
12524
  const ex = args[++i];
12380
12525
  if (ex) result.exclude.push(ex);
12526
+ } else if (arg === "--exclude") {
12527
+ throw new Error("Flag --exclude requires a value");
12381
12528
  } else if (arg === "--no-color") {
12382
12529
  } else if (arg === "--no-animate") {
12383
12530
  result.noAnimate = true;
@@ -12389,6 +12536,8 @@ function parseArgs(args) {
12389
12536
  );
12390
12537
  }
12391
12538
  result.boxStyleOverride = v;
12539
+ } else if (arg === "--style") {
12540
+ throw new Error("Flag --style requires a value");
12392
12541
  } else if (arg === "login") {
12393
12542
  result.command = "login";
12394
12543
  subcommandSeen = true;
@@ -12424,6 +12573,10 @@ function parseArgs(args) {
12424
12573
  subcommandSeen = true;
12425
12574
  } else if (subcommandSeen && !arg.startsWith("-")) {
12426
12575
  result.positionals.push(arg);
12576
+ } else if (arg.startsWith("--")) {
12577
+ throw new Error(`Unknown flag: ${arg}`);
12578
+ } else if (!subcommandSeen) {
12579
+ throw new Error(`Unknown command: ${arg}. Run 'qc --help' for usage.`);
12427
12580
  }
12428
12581
  }
12429
12582
  if (result.messageOnly && result.push) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quikcommit/cli",
3
- "version": "12.1.0",
3
+ "version": "13.1.0",
4
4
  "description": "AI-powered conventional commit messages",
5
5
  "bin": {
6
6
  "qc": "./dist/index.js"
@@ -34,14 +34,14 @@
34
34
  "esbuild": "^0.28.0",
35
35
  "typescript": "^5.9.3",
36
36
  "vitest": "^4.1.5",
37
- "@quikcommit/shared": "9.0.0"
37
+ "@quikcommit/shared": "10.1.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node build.mjs",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "start": "node dist/index.js",
43
43
  "test": "vitest run",
44
- "lint": "eslint .",
45
- "lint:fix": "eslint . --fix"
44
+ "lint": "oxlint .",
45
+ "lint:fix": "oxlint --fix"
46
46
  }
47
47
  }