@quikcommit/cli 1.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 +1583 -0
  2. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,1583 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+
34
+ // ../shared/dist/types.js
35
+ var init_types = __esm({
36
+ "../shared/dist/types.js"() {
37
+ "use strict";
38
+ }
39
+ });
40
+
41
+ // ../shared/dist/constants.js
42
+ var CONFIG_DIR, CREDENTIALS_FILE, CONFIG_FILE, DEFAULT_API_URL, DEVICE_POLL_INTERVAL, DEVICE_FLOW_TIMEOUT;
43
+ var init_constants = __esm({
44
+ "../shared/dist/constants.js"() {
45
+ "use strict";
46
+ CONFIG_DIR = ".config/qc";
47
+ CREDENTIALS_FILE = "credentials";
48
+ CONFIG_FILE = "config.json";
49
+ DEFAULT_API_URL = "https://api.quikcommit.dev";
50
+ DEVICE_POLL_INTERVAL = 2e3;
51
+ DEVICE_FLOW_TIMEOUT = 6e5;
52
+ }
53
+ });
54
+
55
+ // ../shared/dist/rules.js
56
+ var init_rules = __esm({
57
+ "../shared/dist/rules.js"() {
58
+ "use strict";
59
+ }
60
+ });
61
+
62
+ // ../shared/dist/index.js
63
+ var init_dist = __esm({
64
+ "../shared/dist/index.js"() {
65
+ "use strict";
66
+ init_types();
67
+ init_constants();
68
+ init_rules();
69
+ }
70
+ });
71
+
72
+ // src/config.ts
73
+ function getApiKey() {
74
+ const envKey = process.env.QC_API_KEY;
75
+ if (envKey?.trim()) return envKey.trim();
76
+ try {
77
+ if ((0, import_fs.existsSync)(CREDENTIALS_PATH)) {
78
+ return (0, import_fs.readFileSync)(CREDENTIALS_PATH, "utf-8").trim() || null;
79
+ }
80
+ } catch {
81
+ }
82
+ return null;
83
+ }
84
+ function saveApiKey(key) {
85
+ (0, import_fs.mkdirSync)(CONFIG_PATH, { recursive: true, mode: 448 });
86
+ (0, import_fs.writeFileSync)(CREDENTIALS_PATH, key.trim(), { mode: 384 });
87
+ }
88
+ function clearApiKey() {
89
+ try {
90
+ if ((0, import_fs.existsSync)(CREDENTIALS_PATH)) {
91
+ (0, import_fs.unlinkSync)(CREDENTIALS_PATH);
92
+ }
93
+ } catch {
94
+ }
95
+ }
96
+ function getConfig() {
97
+ try {
98
+ if ((0, import_fs.existsSync)(CONFIG_JSON_PATH)) {
99
+ const raw = (0, import_fs.readFileSync)(CONFIG_JSON_PATH, "utf-8");
100
+ return JSON.parse(raw);
101
+ }
102
+ } catch {
103
+ }
104
+ return {};
105
+ }
106
+ function saveConfig(config2) {
107
+ (0, import_fs.mkdirSync)(CONFIG_PATH, { recursive: true, mode: 448 });
108
+ (0, import_fs.writeFileSync)(CONFIG_JSON_PATH, JSON.stringify(config2, null, 2), {
109
+ mode: 384
110
+ });
111
+ }
112
+ var import_fs, import_path, import_os, CONFIG_PATH, CREDENTIALS_PATH, CONFIG_JSON_PATH;
113
+ var init_config = __esm({
114
+ "src/config.ts"() {
115
+ "use strict";
116
+ import_fs = require("fs");
117
+ import_path = require("path");
118
+ import_os = require("os");
119
+ init_dist();
120
+ CONFIG_PATH = (0, import_path.join)((0, import_os.homedir)(), CONFIG_DIR);
121
+ CREDENTIALS_PATH = (0, import_path.join)(CONFIG_PATH, CREDENTIALS_FILE);
122
+ CONFIG_JSON_PATH = (0, import_path.join)(CONFIG_PATH, CONFIG_FILE);
123
+ }
124
+ });
125
+
126
+ // src/api.ts
127
+ var ApiClient;
128
+ var init_api = __esm({
129
+ "src/api.ts"() {
130
+ "use strict";
131
+ init_config();
132
+ init_dist();
133
+ ApiClient = class {
134
+ apiKey;
135
+ baseUrl;
136
+ constructor(options = {}) {
137
+ this.apiKey = options.apiKey ?? getApiKey();
138
+ this.baseUrl = options.baseUrl ?? process.env.QC_API_URL ?? DEFAULT_API_URL;
139
+ }
140
+ hasAuth() {
141
+ return !!this.apiKey?.trim();
142
+ }
143
+ async request(endpoint, body, planRequiredMsg) {
144
+ if (!this.apiKey) {
145
+ throw new Error("Not authenticated. Run `qc login` first.");
146
+ }
147
+ const res = await fetch(`${this.baseUrl}${endpoint}`, {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ Authorization: `Bearer ${this.apiKey}`
152
+ },
153
+ body: JSON.stringify(body)
154
+ });
155
+ if (!res.ok) {
156
+ const err = await res.json().catch(() => ({ error: res.statusText }));
157
+ const code = err.code;
158
+ if (planRequiredMsg && code === "PLAN_REQUIRED") {
159
+ throw new Error(planRequiredMsg);
160
+ }
161
+ throw new Error(err.error ?? `HTTP ${res.status}`);
162
+ }
163
+ return res.json();
164
+ }
165
+ async generateCommit(diff, changes, rules, model) {
166
+ const body = { diff, changes, rules, model };
167
+ const data = await this.request(
168
+ "/v1/commit",
169
+ body
170
+ );
171
+ return { message: data.message ?? "", diagnostics: data.diagnostics };
172
+ }
173
+ async generatePR(req, model) {
174
+ const data = await this.request(
175
+ "/v1/pr",
176
+ { ...req, model },
177
+ "PR descriptions require Pro plan. Upgrade at https://app.quikcommit.dev/billing"
178
+ );
179
+ return { message: data.message ?? "" };
180
+ }
181
+ async generateChangelog(req, model) {
182
+ const data = await this.request(
183
+ "/v1/changelog",
184
+ { ...req, model },
185
+ "Changelog generation requires Pro plan. Upgrade at https://app.quikcommit.dev/billing"
186
+ );
187
+ return { message: data.message ?? "" };
188
+ }
189
+ async fetchJson(endpoint, options) {
190
+ if (!this.apiKey) {
191
+ throw new Error("Not authenticated. Run `qc login` first.");
192
+ }
193
+ const res = await fetch(`${this.baseUrl}${endpoint}`, {
194
+ method: options?.method ?? "GET",
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ Authorization: `Bearer ${this.apiKey}`
198
+ },
199
+ body: options?.body
200
+ });
201
+ if (!res.ok) {
202
+ const err = await res.json().catch(() => ({ error: res.statusText }));
203
+ throw new Error(err.error ?? `HTTP ${res.status}`);
204
+ }
205
+ return res.json();
206
+ }
207
+ async getTeam() {
208
+ return this.fetchJson("/v1/team");
209
+ }
210
+ async getTeamRules() {
211
+ return this.fetchJson("/v1/team/rules");
212
+ }
213
+ async pushTeamRules(rules) {
214
+ await this.fetchJson("/v1/team/rules", {
215
+ method: "PUT",
216
+ body: JSON.stringify(rules)
217
+ });
218
+ }
219
+ async inviteTeamMember(email) {
220
+ await this.fetchJson("/v1/team/invite", {
221
+ method: "POST",
222
+ body: JSON.stringify({ email })
223
+ });
224
+ }
225
+ async getUsage() {
226
+ if (!this.apiKey) return null;
227
+ const res = await fetch(`${this.baseUrl}/v1/usage`, {
228
+ headers: { Authorization: `Bearer ${this.apiKey}` }
229
+ });
230
+ if (!res.ok) return null;
231
+ const data = await res.json();
232
+ return {
233
+ plan: data.plan ?? "free",
234
+ commit_count: data.commit_count ?? 0,
235
+ limit: data.limit ?? 50,
236
+ remaining: data.remaining ?? 50
237
+ };
238
+ }
239
+ };
240
+ }
241
+ });
242
+
243
+ // src/git.ts
244
+ function validateRef(ref, name = "ref") {
245
+ if (!ref || !SAFE_GIT_REF.test(ref)) {
246
+ throw new Error(`Invalid git ref ${name}: "${ref}"`);
247
+ }
248
+ }
249
+ function isGitRepo() {
250
+ try {
251
+ (0, import_child_process.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], {
252
+ stdio: "pipe"
253
+ });
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ function getGitRoot() {
260
+ try {
261
+ return (0, import_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
262
+ encoding: "utf-8"
263
+ }).trim();
264
+ } catch {
265
+ return process.cwd();
266
+ }
267
+ }
268
+ function getStagedDiff(excludes = []) {
269
+ const args = ["diff", "--cached"];
270
+ if (excludes.length > 0) {
271
+ args.push("--");
272
+ args.push(".");
273
+ for (const pattern of excludes) {
274
+ args.push(`:(exclude)${pattern}`);
275
+ }
276
+ }
277
+ return (0, import_child_process.execFileSync)("git", args, {
278
+ encoding: "utf-8",
279
+ maxBuffer: 10 * 1024 * 1024
280
+ });
281
+ }
282
+ function getStagedFiles() {
283
+ return (0, import_child_process.execFileSync)("git", ["diff", "--cached", "--name-only"], {
284
+ encoding: "utf-8"
285
+ });
286
+ }
287
+ function hasStagedChanges() {
288
+ const output = (0, import_child_process.execFileSync)("git", ["diff", "--cached", "--name-only"], {
289
+ encoding: "utf-8"
290
+ });
291
+ return output.trim().length > 0;
292
+ }
293
+ function gitCommit(message) {
294
+ const tmpDir = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os2.tmpdir)(), "qc-"));
295
+ const tmpFile = (0, import_path2.join)(tmpDir, "commit.txt");
296
+ (0, import_fs2.writeFileSync)(tmpFile, message, { mode: 384 });
297
+ try {
298
+ (0, import_child_process.execFileSync)("git", ["commit", "-F", tmpFile], { stdio: "inherit" });
299
+ } finally {
300
+ try {
301
+ (0, import_fs2.unlinkSync)(tmpFile);
302
+ (0, import_fs2.rmdirSync)(tmpDir);
303
+ } catch {
304
+ }
305
+ }
306
+ }
307
+ function gitPush() {
308
+ (0, import_child_process.execFileSync)("git", ["push"], { stdio: "inherit" });
309
+ }
310
+ function getBranchCommits(base = "main") {
311
+ validateRef(base, "base");
312
+ const output = (0, import_child_process.execFileSync)("git", ["log", `${base}..HEAD`, "--format=%s", "--max-count=1000"], {
313
+ encoding: "utf-8",
314
+ maxBuffer: 10 * 1024 * 1024
315
+ });
316
+ return output.trim().split("\n").filter(Boolean);
317
+ }
318
+ function getDiffStat(base = "main") {
319
+ validateRef(base, "base");
320
+ return (0, import_child_process.execFileSync)("git", ["diff", `${base}..HEAD`, "--stat"], {
321
+ encoding: "utf-8",
322
+ maxBuffer: 10 * 1024 * 1024
323
+ });
324
+ }
325
+ function getLatestTag() {
326
+ try {
327
+ return (0, import_child_process.execFileSync)("git", ["describe", "--tags", "--abbrev=0"], {
328
+ encoding: "utf-8"
329
+ }).trim();
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function getCommitsSince(ref, to = "HEAD") {
335
+ validateRef(ref, "from ref");
336
+ validateRef(to, "to ref");
337
+ const output = (0, import_child_process.execFileSync)(
338
+ "git",
339
+ ["log", `${ref}..${to}`, "--format=%H %s", "--max-count=1000"],
340
+ { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
341
+ );
342
+ return output.trim().split("\n").filter(Boolean).map((line) => {
343
+ const [hash, ...rest] = line.split(" ");
344
+ return { hash: hash ?? "", subject: rest.join(" ").trim() };
345
+ });
346
+ }
347
+ var import_child_process, import_fs2, import_path2, import_os2, SAFE_GIT_REF;
348
+ var init_git = __esm({
349
+ "src/git.ts"() {
350
+ "use strict";
351
+ import_child_process = require("child_process");
352
+ import_fs2 = require("fs");
353
+ import_path2 = require("path");
354
+ import_os2 = require("os");
355
+ SAFE_GIT_REF = /^[a-zA-Z0-9._\-/~:^@]+$/;
356
+ }
357
+ });
358
+
359
+ // src/monorepo.ts
360
+ function findGitRoot(start) {
361
+ try {
362
+ return (0, import_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
363
+ encoding: "utf-8",
364
+ cwd: start,
365
+ stdio: ["pipe", "pipe", "pipe"]
366
+ }).trim();
367
+ } catch {
368
+ return start;
369
+ }
370
+ }
371
+ function detectWorkspace(cwd = findGitRoot(process.cwd())) {
372
+ const pnpmWs = (0, import_path3.join)(cwd, "pnpm-workspace.yaml");
373
+ if ((0, import_fs3.existsSync)(pnpmWs)) {
374
+ const content = (0, import_fs3.readFileSync)(pnpmWs, "utf-8");
375
+ const match = content.match(/packages:\s*\n((?:\s+-\s+.+\n?)*)/);
376
+ if (match) {
377
+ const packages = match[1].split("\n").map((l) => l.replace(/^\s+-\s+/, "").replace(/["']/g, "").trim()).filter(Boolean);
378
+ return { type: "pnpm", packages, root: cwd };
379
+ }
380
+ }
381
+ const lerna = (0, import_path3.join)(cwd, "lerna.json");
382
+ if ((0, import_fs3.existsSync)(lerna)) {
383
+ try {
384
+ const config2 = JSON.parse((0, import_fs3.readFileSync)(lerna, "utf-8"));
385
+ return {
386
+ type: "lerna",
387
+ packages: config2.packages ?? ["packages/*"],
388
+ root: cwd
389
+ };
390
+ } catch {
391
+ }
392
+ }
393
+ if ((0, import_fs3.existsSync)((0, import_path3.join)(cwd, "nx.json"))) {
394
+ return {
395
+ type: "nx",
396
+ packages: ["packages/*", "apps/*", "libs/*"],
397
+ root: cwd
398
+ };
399
+ }
400
+ if ((0, import_fs3.existsSync)((0, import_path3.join)(cwd, "turbo.json"))) {
401
+ const pkgPath2 = (0, import_path3.join)(cwd, "package.json");
402
+ if ((0, import_fs3.existsSync)(pkgPath2)) {
403
+ try {
404
+ const config2 = JSON.parse((0, import_fs3.readFileSync)(pkgPath2, "utf-8"));
405
+ if (config2.workspaces) {
406
+ const ws = Array.isArray(config2.workspaces) ? config2.workspaces : config2.workspaces.packages ?? [];
407
+ return { type: "turbo", packages: ws, root: cwd };
408
+ }
409
+ } catch {
410
+ }
411
+ }
412
+ }
413
+ const pkgPath = (0, import_path3.join)(cwd, "package.json");
414
+ if ((0, import_fs3.existsSync)(pkgPath)) {
415
+ try {
416
+ const config2 = JSON.parse((0, import_fs3.readFileSync)(pkgPath, "utf-8"));
417
+ if (config2.workspaces) {
418
+ const ws = Array.isArray(config2.workspaces) ? config2.workspaces : config2.workspaces.packages ?? [];
419
+ return { type: "npm", packages: ws, root: cwd };
420
+ }
421
+ } catch {
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+ function matchGlobPattern(rel, pattern) {
427
+ const dir = pattern.replace(/\/?\*\*?$/, "").replace(/\/$/, "");
428
+ if (!dir || dir === "*" || dir === "**") {
429
+ const pkg = rel.split("/")[0];
430
+ return pkg || null;
431
+ }
432
+ const starIdx = dir.indexOf("*");
433
+ if (starIdx !== -1) {
434
+ const prefix2 = dir.slice(0, starIdx);
435
+ if (rel.startsWith(prefix2)) {
436
+ const rest = rel.slice(prefix2.length);
437
+ const pkg = rest.split("/")[0];
438
+ return pkg || null;
439
+ }
440
+ return null;
441
+ }
442
+ const prefix = dir + "/";
443
+ if (rel.startsWith(prefix)) {
444
+ const rest = rel.slice(prefix.length);
445
+ const pkg = rest.split("/")[0];
446
+ return pkg || null;
447
+ }
448
+ return null;
449
+ }
450
+ function getPackageForFile(filePath, workspace) {
451
+ const absPath = filePath.startsWith("/") ? filePath : (0, import_path3.join)(workspace.root, filePath);
452
+ const rel = (0, import_path3.relative)(workspace.root, absPath);
453
+ for (const pattern of workspace.packages) {
454
+ const packageName = matchGlobPattern(rel, pattern);
455
+ if (packageName) return packageName;
456
+ }
457
+ return null;
458
+ }
459
+ function autoDetectScope(stagedFiles, workspace) {
460
+ const packages = /* @__PURE__ */ new Set();
461
+ for (const file of stagedFiles) {
462
+ const filePath = file.startsWith("/") ? file : (0, import_path3.join)(workspace.root, file);
463
+ const pkg = getPackageForFile(filePath, workspace);
464
+ if (pkg) packages.add(pkg);
465
+ }
466
+ if (packages.size === 1) return [...packages][0];
467
+ if (packages.size > 1 && packages.size <= 3) return [...packages].join(",");
468
+ if (packages.size > 3) {
469
+ console.error(
470
+ `[qc] Changes span ${packages.size} packages; skipping auto-scope detection.`
471
+ );
472
+ }
473
+ return null;
474
+ }
475
+ var import_child_process2, import_fs3, import_path3;
476
+ var init_monorepo = __esm({
477
+ "src/monorepo.ts"() {
478
+ "use strict";
479
+ import_child_process2 = require("child_process");
480
+ import_fs3 = require("fs");
481
+ import_path3 = require("path");
482
+ }
483
+ });
484
+
485
+ // src/commands/login.ts
486
+ var login_exports = {};
487
+ __export(login_exports, {
488
+ runLogin: () => runLogin
489
+ });
490
+ function openBrowser(url) {
491
+ try {
492
+ if ((0, import_os3.platform)() === "darwin") {
493
+ (0, import_child_process3.execFileSync)("open", [url], { stdio: "pipe" });
494
+ return true;
495
+ }
496
+ if ((0, import_os3.platform)() === "linux") {
497
+ (0, import_child_process3.execFileSync)("xdg-open", [url], { stdio: "pipe" });
498
+ return true;
499
+ }
500
+ if ((0, import_os3.platform)() === "win32") {
501
+ (0, import_child_process3.execFileSync)("cmd", ["/c", "start", "", url], { stdio: "pipe" });
502
+ return true;
503
+ }
504
+ } catch {
505
+ }
506
+ return false;
507
+ }
508
+ async function runLogin() {
509
+ const startRes = await fetch(`${API_URL}/v1/auth/device/start`, {
510
+ method: "POST",
511
+ headers: { "Content-Type": "application/json" },
512
+ body: JSON.stringify({})
513
+ });
514
+ if (!startRes.ok) {
515
+ const err = await startRes.json().catch(() => ({ error: startRes.statusText }));
516
+ throw new Error(err.error ?? "Failed to start device flow");
517
+ }
518
+ const startData = await startRes.json();
519
+ const code = startData.device_code;
520
+ if (!code) {
521
+ throw new Error("Server did not return a device_code");
522
+ }
523
+ console.log("Opening browser to sign in...");
524
+ console.log("");
525
+ const authUrl = `${DASHBOARD_URL}/auth/cli?code=${encodeURIComponent(code)}`;
526
+ const opened = openBrowser(authUrl);
527
+ if (!opened) {
528
+ console.log("Could not open browser. Please visit:");
529
+ console.log(authUrl);
530
+ console.log("");
531
+ }
532
+ const startTime = Date.now();
533
+ while (Date.now() - startTime < DEVICE_FLOW_TIMEOUT) {
534
+ try {
535
+ const res = await fetch(
536
+ `${API_URL}/v1/auth/device/poll?code=${encodeURIComponent(code)}`
537
+ );
538
+ const data = await res.json();
539
+ if (data.status === "complete" && data.api_key) {
540
+ saveApiKey(data.api_key);
541
+ console.log("Successfully logged in!");
542
+ return;
543
+ }
544
+ } catch {
545
+ }
546
+ await new Promise((r) => setTimeout(r, DEVICE_POLL_INTERVAL));
547
+ }
548
+ console.error("Login timed out. Please try again.");
549
+ process.exit(1);
550
+ }
551
+ var import_child_process3, import_os3, API_URL, DASHBOARD_URL;
552
+ var init_login = __esm({
553
+ "src/commands/login.ts"() {
554
+ "use strict";
555
+ import_child_process3 = require("child_process");
556
+ import_os3 = require("os");
557
+ init_config();
558
+ init_dist();
559
+ API_URL = process.env.QC_API_URL ?? DEFAULT_API_URL;
560
+ DASHBOARD_URL = "https://app.quikcommit.dev";
561
+ }
562
+ });
563
+
564
+ // src/commands/logout.ts
565
+ var logout_exports = {};
566
+ __export(logout_exports, {
567
+ runLogout: () => runLogout
568
+ });
569
+ function runLogout() {
570
+ clearApiKey();
571
+ console.log("Logged out. Credentials cleared.");
572
+ }
573
+ var init_logout = __esm({
574
+ "src/commands/logout.ts"() {
575
+ "use strict";
576
+ init_config();
577
+ }
578
+ });
579
+
580
+ // src/commands/status.ts
581
+ var status_exports = {};
582
+ __export(status_exports, {
583
+ runStatus: () => runStatus
584
+ });
585
+ async function runStatus(apiKeyFlag) {
586
+ const apiKey = apiKeyFlag ?? getApiKey();
587
+ if (!apiKey) {
588
+ console.log("Not logged in. Run `qc login` to authenticate.");
589
+ return;
590
+ }
591
+ console.log("Logged in: yes");
592
+ console.log(` API key: ...${apiKey.slice(-4)}`);
593
+ const client = new ApiClient({ apiKey });
594
+ const usage = await client.getUsage();
595
+ if (usage) {
596
+ console.log(`Plan: ${usage.plan}`);
597
+ console.log(`Usage: ${usage.commit_count}/${usage.limit} commits this period`);
598
+ console.log(`Remaining: ${usage.remaining}`);
599
+ } else {
600
+ console.log("Usage: (unable to fetch)");
601
+ }
602
+ }
603
+ var init_status = __esm({
604
+ "src/commands/status.ts"() {
605
+ "use strict";
606
+ init_config();
607
+ init_api();
608
+ }
609
+ });
610
+
611
+ // src/commands/pr.ts
612
+ var pr_exports = {};
613
+ __export(pr_exports, {
614
+ pr: () => pr
615
+ });
616
+ async function pr(options) {
617
+ const base = options.base ?? "main";
618
+ const commits = getBranchCommits(base);
619
+ const diffStat = getDiffStat(base);
620
+ if (commits.length === 0) {
621
+ console.error(`No commits found on this branch vs ${base}`);
622
+ process.exit(1);
623
+ }
624
+ console.error(`Generating PR description from ${commits.length} commits...`);
625
+ const apiKey = getApiKey();
626
+ if (!apiKey) {
627
+ console.error("Error: Not authenticated. Run `qc login` first.");
628
+ process.exit(1);
629
+ }
630
+ const client = new ApiClient({ apiKey });
631
+ const result = await client.generatePR(
632
+ {
633
+ commits,
634
+ diff_stat: diffStat,
635
+ base_branch: base
636
+ },
637
+ options.model
638
+ );
639
+ console.log("\n" + result.message + "\n");
640
+ if (options.create) {
641
+ try {
642
+ const title = result.message.split("\n").find((l) => l.trim()) ?? result.message.substring(0, 72).trim();
643
+ (0, import_child_process4.execFileSync)("gh", ["pr", "create", "--title", title, "--body", result.message], {
644
+ stdio: "inherit"
645
+ });
646
+ } catch {
647
+ console.error("Error: `gh` CLI not found or failed. Install from https://cli.github.com/");
648
+ process.exit(1);
649
+ }
650
+ }
651
+ }
652
+ var import_child_process4;
653
+ var init_pr = __esm({
654
+ "src/commands/pr.ts"() {
655
+ "use strict";
656
+ import_child_process4 = require("child_process");
657
+ init_config();
658
+ init_api();
659
+ init_git();
660
+ }
661
+ });
662
+
663
+ // src/commands/changelog.ts
664
+ var changelog_exports = {};
665
+ __export(changelog_exports, {
666
+ changelog: () => changelog
667
+ });
668
+ function parseCommitType(subject) {
669
+ const match = subject.match(CONVENTIONAL_TYPE_RE);
670
+ return match ? match[1].toLowerCase() : "chore";
671
+ }
672
+ function groupCommitsByType(commits) {
673
+ const byType = {};
674
+ for (const { subject } of commits) {
675
+ const type = parseCommitType(subject);
676
+ if (!byType[type]) byType[type] = [];
677
+ byType[type].push(subject);
678
+ }
679
+ return byType;
680
+ }
681
+ async function changelog(options) {
682
+ const fromRef = options.from ?? getLatestTag();
683
+ const toRef = options.to ?? "HEAD";
684
+ if (!fromRef) {
685
+ console.error("Error: No git tag found. Use --from <ref> to specify a starting point.");
686
+ process.exit(1);
687
+ }
688
+ const commits = getCommitsSince(fromRef, toRef);
689
+ if (commits.length === 0) {
690
+ console.error(`No commits found between ${fromRef} and ${toRef}`);
691
+ process.exit(1);
692
+ }
693
+ const commitsByType = groupCommitsByType(commits);
694
+ const apiKey = getApiKey();
695
+ if (!apiKey) {
696
+ console.error("Error: Not authenticated. Run `qc login` first.");
697
+ process.exit(1);
698
+ }
699
+ const client = new ApiClient({ apiKey });
700
+ const result = await client.generateChangelog(
701
+ {
702
+ commits_by_type: commitsByType,
703
+ from_tag: fromRef,
704
+ to_ref: toRef
705
+ },
706
+ options.model
707
+ );
708
+ const version = options.version ?? (/^v?\d/.test(toRef) && toRef !== "HEAD" ? toRef.replace(/^v/, "") : null) ?? `${fromRef.replace(/^v/, "")}-next`;
709
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
710
+ const header = `## [${version}] - ${date}
711
+
712
+ `;
713
+ const changelogEntry = header + result.message;
714
+ if (options.write) {
715
+ const path = (0, import_path4.join)(getGitRoot(), "CHANGELOG.md");
716
+ const existing = (0, import_fs4.existsSync)(path) ? (0, import_fs4.readFileSync)(path, "utf-8") : "";
717
+ const newContent = changelogEntry + (existing ? "\n\n" + existing : "");
718
+ (0, import_fs4.writeFileSync)(path, newContent);
719
+ console.error(`Wrote to ${path}`);
720
+ } else {
721
+ console.log(changelogEntry);
722
+ }
723
+ }
724
+ var import_fs4, import_path4, CONVENTIONAL_TYPE_RE;
725
+ var init_changelog = __esm({
726
+ "src/commands/changelog.ts"() {
727
+ "use strict";
728
+ import_fs4 = require("fs");
729
+ import_path4 = require("path");
730
+ init_config();
731
+ init_api();
732
+ init_git();
733
+ CONVENTIONAL_TYPE_RE = /^(feat|fix|docs|style|refactor|perf|test|chore)(\([^)]+\))?!?:\s+/i;
734
+ }
735
+ });
736
+
737
+ // src/commands/init.ts
738
+ var init_exports = {};
739
+ __export(init_exports, {
740
+ init: () => init
741
+ });
742
+ async function init(options) {
743
+ let hooksDir;
744
+ try {
745
+ hooksDir = (0, import_child_process5.execFileSync)("git", ["rev-parse", "--git-path", "hooks"], {
746
+ encoding: "utf-8"
747
+ }).trim();
748
+ } catch {
749
+ console.error("Error: Not a git repository");
750
+ process.exit(1);
751
+ }
752
+ const hookPath = (0, import_path5.join)(hooksDir, "prepare-commit-msg");
753
+ if (options.uninstall) {
754
+ if ((0, import_fs5.existsSync)(hookPath)) {
755
+ const content = (0, import_fs5.readFileSync)(hookPath, "utf-8");
756
+ if (content.includes("QuikCommit")) {
757
+ (0, import_fs5.unlinkSync)(hookPath);
758
+ console.log("QuikCommit hook removed.");
759
+ } else {
760
+ console.log("Hook exists but was not installed by QuikCommit. Skipping.");
761
+ }
762
+ } else {
763
+ console.log("No hook to remove.");
764
+ }
765
+ return;
766
+ }
767
+ if ((0, import_fs5.existsSync)(hookPath)) {
768
+ const content = (0, import_fs5.readFileSync)(hookPath, "utf-8");
769
+ if (content.includes("QuikCommit")) {
770
+ console.log("QuikCommit hook is already installed.");
771
+ return;
772
+ }
773
+ console.error(
774
+ "A prepare-commit-msg hook already exists. Use --uninstall first or manually merge."
775
+ );
776
+ process.exit(1);
777
+ }
778
+ (0, import_fs5.writeFileSync)(hookPath, HOOK_CONTENT);
779
+ (0, import_fs5.chmodSync)(hookPath, 493);
780
+ console.log("QuikCommit hook installed.");
781
+ console.log("Now just run `git commit` and a message will be generated automatically.");
782
+ }
783
+ var import_fs5, import_path5, import_child_process5, HOOK_CONTENT;
784
+ var init_init = __esm({
785
+ "src/commands/init.ts"() {
786
+ "use strict";
787
+ import_fs5 = require("fs");
788
+ import_path5 = require("path");
789
+ import_child_process5 = require("child_process");
790
+ HOOK_CONTENT = `#!/bin/sh
791
+ # QuikCommit - auto-generate commit messages
792
+ # Installed by: qc init
793
+ # Remove with: qc init --uninstall
794
+
795
+ # Only generate if no message was provided (empty commit message file)
796
+ COMMIT_MSG_FILE="$1"
797
+ COMMIT_SOURCE="$2"
798
+
799
+ # Skip if message was provided via -m, merge, squash, etc.
800
+ if [ -n "$COMMIT_SOURCE" ]; then
801
+ exit 0
802
+ fi
803
+
804
+ # Skip if message file already has content (excluding comments)
805
+ if grep -qv '^#' "$COMMIT_MSG_FILE" 2>/dev/null; then
806
+ if [ -n "$(grep -v '^#' "$COMMIT_MSG_FILE" | grep -v '^$')" ]; then
807
+ exit 0
808
+ fi
809
+ fi
810
+
811
+ # Generate commit message
812
+ MSG=$(qc --message-only --hook-mode 2>/dev/null)
813
+ if [ $? -eq 0 ] && [ -n "$MSG" ]; then
814
+ printf '%s
815
+ ' "$MSG" > "$COMMIT_MSG_FILE"
816
+ fi
817
+ `;
818
+ }
819
+ });
820
+
821
+ // src/commands/team.ts
822
+ var team_exports = {};
823
+ __export(team_exports, {
824
+ team: () => team
825
+ });
826
+ function createApiClient() {
827
+ return new ApiClient();
828
+ }
829
+ function mapCommitlintToRules(config2) {
830
+ if (!config2 || typeof config2 !== "object") return null;
831
+ const c = config2;
832
+ const rules = {};
833
+ const ext = c.extends;
834
+ const rulesConfig = c.rules;
835
+ if (Array.isArray(rulesConfig?.["type-enum"]) && rulesConfig["type-enum"].length >= 3) {
836
+ const [, , value] = rulesConfig["type-enum"];
837
+ if (Array.isArray(value)) rules.types = value;
838
+ }
839
+ if (Array.isArray(rulesConfig?.["scope-enum"]) && rulesConfig["scope-enum"].length >= 3) {
840
+ const [, , value] = rulesConfig["scope-enum"];
841
+ if (Array.isArray(value)) rules.scopes = value;
842
+ }
843
+ if (Array.isArray(rulesConfig?.["header-max-length"]) && rulesConfig["header-max-length"].length >= 3) {
844
+ const [, , maxLen] = rulesConfig["header-max-length"];
845
+ if (typeof maxLen === "number") rules.headerMaxLength = maxLen;
846
+ }
847
+ if (Array.isArray(rulesConfig?.["subject-case"]) && rulesConfig["subject-case"].length >= 3) {
848
+ const [, , val] = rulesConfig["subject-case"];
849
+ if (val != null) rules.subjectCase = Array.isArray(val) ? val : [val];
850
+ }
851
+ return Object.keys(rules).length > 0 ? rules : null;
852
+ }
853
+ function detectLocalCommitlintRules() {
854
+ const cwd = process.cwd();
855
+ const files = [
856
+ ".commitlintrc.json",
857
+ ".commitlintrc",
858
+ "commitlint.config.js",
859
+ "commitlint.config.cjs",
860
+ "commitlint.config.mjs"
861
+ ];
862
+ for (const file of files) {
863
+ const path = (0, import_path6.join)(cwd, file);
864
+ if (!(0, import_fs6.existsSync)(path)) continue;
865
+ try {
866
+ const content = (0, import_fs6.readFileSync)(path, "utf-8");
867
+ let parsed;
868
+ if (file.endsWith(".json") || file === ".commitlintrc") {
869
+ parsed = JSON.parse(content);
870
+ } else {
871
+ continue;
872
+ }
873
+ const rules = mapCommitlintToRules(parsed);
874
+ if (rules) return rules;
875
+ } catch {
876
+ }
877
+ }
878
+ const pkgPath = (0, import_path6.join)(cwd, "package.json");
879
+ if ((0, import_fs6.existsSync)(pkgPath)) {
880
+ try {
881
+ const content = (0, import_fs6.readFileSync)(pkgPath, "utf-8");
882
+ const pkg = JSON.parse(content);
883
+ if (pkg.commitlint) {
884
+ const rules = mapCommitlintToRules(pkg.commitlint);
885
+ if (rules) return rules;
886
+ }
887
+ } catch {
888
+ }
889
+ }
890
+ const config2 = getConfig();
891
+ if (config2.rules && Object.keys(config2.rules).length > 0) {
892
+ return config2.rules;
893
+ }
894
+ return null;
895
+ }
896
+ async function team(subcommand, args) {
897
+ const api = createApiClient();
898
+ switch (subcommand) {
899
+ case void 0:
900
+ case "info": {
901
+ const info = await api.getTeam();
902
+ console.log(`
903
+ Team: ${info.name}`);
904
+ console.log(` Plan: ${info.plan}`);
905
+ console.log(` Members: ${info.member_count}`);
906
+ console.log("\n Members:");
907
+ for (const m of info.members) {
908
+ console.log(` ${m.name ?? m.email} <${m.email}> (${m.role})`);
909
+ }
910
+ break;
911
+ }
912
+ case "rules": {
913
+ if (args?.[0] === "push") {
914
+ const rules = detectLocalCommitlintRules();
915
+ if (!rules) {
916
+ console.error("No local commitlint config found.");
917
+ process.exit(1);
918
+ }
919
+ await api.pushTeamRules(rules);
920
+ console.log("Team rules updated from local commitlint config.");
921
+ } else {
922
+ const rules = await api.getTeamRules();
923
+ console.log("\n Team Commit Rules:");
924
+ console.log(JSON.stringify(rules, null, 2));
925
+ }
926
+ break;
927
+ }
928
+ case "invite": {
929
+ const email = args?.[0];
930
+ if (!email) {
931
+ console.error("Usage: qc team invite <email>");
932
+ process.exit(1);
933
+ }
934
+ await api.inviteTeamMember(email);
935
+ console.log(`Invitation sent to ${email}`);
936
+ break;
937
+ }
938
+ default:
939
+ console.error(`Unknown team command: ${subcommand}`);
940
+ console.log("Usage: qc team [info|rules|rules push|invite <email>]");
941
+ process.exit(1);
942
+ }
943
+ }
944
+ var import_fs6, import_path6;
945
+ var init_team = __esm({
946
+ "src/commands/team.ts"() {
947
+ "use strict";
948
+ import_fs6 = require("fs");
949
+ import_path6 = require("path");
950
+ init_api();
951
+ init_config();
952
+ }
953
+ });
954
+
955
+ // src/commands/config.ts
956
+ var config_exports = {};
957
+ __export(config_exports, {
958
+ config: () => config
959
+ });
960
+ async function config(args) {
961
+ if (args.length === 0) {
962
+ showConfig();
963
+ return;
964
+ }
965
+ const sub = args[0];
966
+ if (sub === "set") {
967
+ const key = args[1];
968
+ const value = args[2];
969
+ if (!key || !value) {
970
+ console.error("Usage: qc config set <key> <value>");
971
+ console.error(" Keys: model, api_url, provider");
972
+ process.exit(1);
973
+ }
974
+ await setConfig(key, value);
975
+ return;
976
+ }
977
+ if (sub === "reset") {
978
+ resetConfig();
979
+ return;
980
+ }
981
+ console.error(`Unknown subcommand: ${sub}`);
982
+ console.error("Usage: qc config [set <key> <value> | reset]");
983
+ process.exit(1);
984
+ }
985
+ function showConfig() {
986
+ const cfg = getConfig();
987
+ const apiKey = getApiKey();
988
+ console.log("Current configuration:");
989
+ console.log(` model: ${cfg.model ?? "(default for plan)"}`);
990
+ console.log(` api_url: ${cfg.apiUrl ?? DEFAULT_API_URL}`);
991
+ console.log(` provider: ${cfg.provider ?? "(default)"}`);
992
+ console.log(` auth: ${apiKey ? "****" : "not set"}`);
993
+ if (cfg.excludes?.length) {
994
+ console.log(` excludes: ${cfg.excludes.join(", ")}`);
995
+ }
996
+ }
997
+ async function setConfig(key, value) {
998
+ const cfg = getConfig();
999
+ const updates = {};
1000
+ if (key === "model") {
1001
+ updates.model = value;
1002
+ } else if (key === "provider") {
1003
+ const valid = ["ollama", "lmstudio", "openrouter", "custom", "cloudflare"];
1004
+ if (!valid.includes(value.toLowerCase())) {
1005
+ console.error(`Invalid provider. Must be one of: ${valid.join(", ")}`);
1006
+ process.exit(1);
1007
+ }
1008
+ updates.provider = value.toLowerCase();
1009
+ } else if (key === "api_url") {
1010
+ try {
1011
+ new URL(value);
1012
+ updates.apiUrl = value;
1013
+ } catch {
1014
+ console.error("Invalid URL:", value);
1015
+ process.exit(1);
1016
+ }
1017
+ } else {
1018
+ console.error(`Unknown key: ${key}`);
1019
+ console.error(" Keys: model, api_url, provider");
1020
+ process.exit(1);
1021
+ }
1022
+ saveConfig({ ...cfg, ...updates });
1023
+ console.log(`Set ${key} = ${value}`);
1024
+ }
1025
+ function resetConfig() {
1026
+ saveConfig({});
1027
+ console.log("Config reset to defaults.");
1028
+ }
1029
+ var init_config2 = __esm({
1030
+ "src/commands/config.ts"() {
1031
+ "use strict";
1032
+ init_config();
1033
+ init_dist();
1034
+ }
1035
+ });
1036
+
1037
+ // src/commands/upgrade.ts
1038
+ var upgrade_exports = {};
1039
+ __export(upgrade_exports, {
1040
+ upgrade: () => upgrade
1041
+ });
1042
+ async function upgrade() {
1043
+ console.log(`
1044
+ Opening ${BILLING_URL}
1045
+ `);
1046
+ try {
1047
+ const { execFileSync: execFileSync6 } = await import("child_process");
1048
+ if (process.platform === "darwin") {
1049
+ execFileSync6("open", [BILLING_URL]);
1050
+ } else if (process.platform === "linux") {
1051
+ execFileSync6("xdg-open", [BILLING_URL]);
1052
+ } else if (process.platform === "win32") {
1053
+ execFileSync6("cmd", ["/c", "start", "", BILLING_URL]);
1054
+ }
1055
+ } catch {
1056
+ console.log(`Visit: ${BILLING_URL}`);
1057
+ }
1058
+ }
1059
+ var BILLING_URL;
1060
+ var init_upgrade = __esm({
1061
+ "src/commands/upgrade.ts"() {
1062
+ "use strict";
1063
+ BILLING_URL = "https://app.quikcommit.dev/billing";
1064
+ }
1065
+ });
1066
+
1067
+ // src/local.ts
1068
+ var local_exports = {};
1069
+ __export(local_exports, {
1070
+ getLocalProviderConfig: () => getLocalProviderConfig,
1071
+ runLocalCommit: () => runLocalCommit
1072
+ });
1073
+ function getLegacyProvider() {
1074
+ try {
1075
+ const p = (0, import_path7.join)(CONFIG_PATH2, "provider");
1076
+ if ((0, import_fs7.existsSync)(p)) {
1077
+ const v = (0, import_fs7.readFileSync)(p, "utf-8").trim().toLowerCase();
1078
+ if (["ollama", "lmstudio", "openrouter", "custom", "cloudflare"].includes(v)) {
1079
+ return v;
1080
+ }
1081
+ }
1082
+ } catch {
1083
+ }
1084
+ return null;
1085
+ }
1086
+ function getLegacyBaseUrl(provider) {
1087
+ try {
1088
+ const p = (0, import_path7.join)(CONFIG_PATH2, "base_url");
1089
+ if ((0, import_fs7.existsSync)(p)) {
1090
+ return (0, import_fs7.readFileSync)(p, "utf-8").trim();
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ return PROVIDER_URLS[provider] ?? "";
1095
+ }
1096
+ function getLegacyModel(provider) {
1097
+ try {
1098
+ const p = (0, import_path7.join)(CONFIG_PATH2, "model");
1099
+ if ((0, import_fs7.existsSync)(p)) {
1100
+ const v = (0, import_fs7.readFileSync)(p, "utf-8").trim();
1101
+ if (v) return v;
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ return DEFAULT_MODELS[provider] ?? "";
1106
+ }
1107
+ function getLocalProviderConfig() {
1108
+ const config2 = getConfig();
1109
+ const provider = config2.provider ?? getLegacyProvider();
1110
+ if (!provider) return null;
1111
+ const baseUrl = config2.apiUrl ?? getLegacyBaseUrl(provider) ?? PROVIDER_URLS[provider] ?? "";
1112
+ if (!baseUrl) return null;
1113
+ const model = config2.model ?? getLegacyModel(provider) ?? DEFAULT_MODELS[provider];
1114
+ const apiKey = provider === "openrouter" || provider === "custom" ? getApiKey() : null;
1115
+ if (provider === "openrouter" && !apiKey) return null;
1116
+ return { provider, baseUrl, model, apiKey };
1117
+ }
1118
+ function buildUserPrompt(changes, diff, rules) {
1119
+ let prompt = `Generate a commit message for these changes:
1120
+
1121
+ ## File changes:
1122
+ <file_changes>
1123
+ ${changes}
1124
+ </file_changes>
1125
+
1126
+ ## Diff:
1127
+ <diff>
1128
+ ${diff}
1129
+ </diff>
1130
+
1131
+ `;
1132
+ if (rules && Object.keys(rules).length > 0) {
1133
+ prompt += `Rules: ${JSON.stringify(rules)}
1134
+
1135
+ `;
1136
+ }
1137
+ prompt += `Important:
1138
+ - Follow conventional commit format: <type>(<scope>): <subject>
1139
+ - Response should be the commit message only, no explanations`;
1140
+ return prompt;
1141
+ }
1142
+ function buildRequest(provider, baseUrl, userContent, diff, changes, model, apiKey, rules) {
1143
+ const headers = {
1144
+ "Content-Type": "application/json"
1145
+ };
1146
+ if (apiKey) {
1147
+ headers["Authorization"] = `Bearer ${apiKey}`;
1148
+ }
1149
+ if (provider === "openrouter") {
1150
+ headers["HTTP-Referer"] = "https://github.com/quikcommit/quikcommit";
1151
+ headers["X-Title"] = "qc - AI Commit Message Generator";
1152
+ }
1153
+ let url;
1154
+ let body;
1155
+ switch (provider) {
1156
+ case "ollama":
1157
+ url = `${baseUrl}/api/generate`;
1158
+ body = {
1159
+ model,
1160
+ prompt: userContent,
1161
+ stream: false,
1162
+ options: {}
1163
+ };
1164
+ return { url, body, headers: { "Content-Type": "application/json" } };
1165
+ case "lmstudio":
1166
+ url = `${baseUrl}/chat/completions`;
1167
+ body = {
1168
+ model,
1169
+ stream: false,
1170
+ messages: [
1171
+ {
1172
+ role: "system",
1173
+ content: "You are a git commit message generator. Create conventional commit messages."
1174
+ },
1175
+ { role: "user", content: userContent }
1176
+ ]
1177
+ };
1178
+ return { url, body, headers: { "Content-Type": "application/json" } };
1179
+ case "openrouter":
1180
+ case "custom":
1181
+ url = `${baseUrl}/chat/completions`;
1182
+ body = {
1183
+ model,
1184
+ stream: false,
1185
+ messages: [
1186
+ {
1187
+ role: "system",
1188
+ content: "You are a git commit message generator. Create conventional commit messages."
1189
+ },
1190
+ { role: "user", content: userContent }
1191
+ ]
1192
+ };
1193
+ return { url, body, headers };
1194
+ case "cloudflare":
1195
+ url = `${baseUrl.replace(/\/$/, "")}/commit`;
1196
+ body = { diff, changes, rules };
1197
+ return { url, body, headers: { "Content-Type": "application/json" } };
1198
+ default:
1199
+ throw new Error(`Unknown provider: ${provider}`);
1200
+ }
1201
+ }
1202
+ function parseResponse(provider, data) {
1203
+ const r = data;
1204
+ switch (provider) {
1205
+ case "ollama":
1206
+ return r.response ?? "";
1207
+ case "lmstudio":
1208
+ case "openrouter":
1209
+ case "custom": {
1210
+ const choices = r.choices;
1211
+ return choices?.[0]?.message?.content ?? "";
1212
+ }
1213
+ case "cloudflare":
1214
+ return r.commit?.response ?? "";
1215
+ default:
1216
+ return "";
1217
+ }
1218
+ }
1219
+ async function runLocalCommit(messageOnly, push, modelFlag) {
1220
+ if (!isGitRepo()) {
1221
+ throw new Error("Not a git repository.");
1222
+ }
1223
+ if (!hasStagedChanges()) {
1224
+ throw new Error("No staged changes. Stage files with `git add` first.");
1225
+ }
1226
+ const local = getLocalProviderConfig();
1227
+ if (!local) {
1228
+ throw new Error(
1229
+ "No local provider configured. Set provider in ~/.config/qc/config.json or run with SaaS (qc login)."
1230
+ );
1231
+ }
1232
+ const config2 = getConfig();
1233
+ const excludes = config2.excludes ?? [];
1234
+ const diff = getStagedDiff(excludes);
1235
+ const changes = getStagedFiles();
1236
+ const model = modelFlag ?? local.model;
1237
+ let rules = config2.rules ?? {};
1238
+ const workspace = detectWorkspace();
1239
+ if (workspace) {
1240
+ const stagedFiles = changes.trim().split("\n").filter(Boolean);
1241
+ const scope = autoDetectScope(stagedFiles, workspace);
1242
+ if (scope) {
1243
+ const scopes = scope.split(",").map((s) => s.trim());
1244
+ rules = { ...rules, scopes };
1245
+ }
1246
+ }
1247
+ const userContent = buildUserPrompt(changes, diff, rules);
1248
+ const { url, body, headers } = buildRequest(
1249
+ local.provider,
1250
+ local.baseUrl,
1251
+ userContent,
1252
+ diff,
1253
+ changes,
1254
+ model,
1255
+ local.apiKey,
1256
+ rules
1257
+ );
1258
+ if (!url || url.includes("YOUR-WORKER")) {
1259
+ throw new Error(
1260
+ "Cloudflare provider requires api_url. Run: qc config set api_url https://your-worker.workers.dev"
1261
+ );
1262
+ }
1263
+ const res = await fetch(url, {
1264
+ method: "POST",
1265
+ headers,
1266
+ body: JSON.stringify(body)
1267
+ });
1268
+ if (!res.ok) {
1269
+ const text = await res.text();
1270
+ throw new Error(`Provider error (${res.status}): ${text}`);
1271
+ }
1272
+ const data = await res.json();
1273
+ let message = parseResponse(local.provider, data);
1274
+ message = message.replace(/\\n/g, "\n").replace(/\\r/g, "").trim();
1275
+ if (!message) {
1276
+ throw new Error("Failed to generate commit message.");
1277
+ }
1278
+ if (messageOnly) {
1279
+ console.log(message);
1280
+ return;
1281
+ }
1282
+ gitCommit(message);
1283
+ if (push) {
1284
+ gitPush();
1285
+ }
1286
+ }
1287
+ var import_fs7, import_path7, import_os4, CONFIG_PATH2, PROVIDER_URLS, DEFAULT_MODELS;
1288
+ var init_local = __esm({
1289
+ "src/local.ts"() {
1290
+ "use strict";
1291
+ import_fs7 = require("fs");
1292
+ import_path7 = require("path");
1293
+ import_os4 = require("os");
1294
+ init_config();
1295
+ init_dist();
1296
+ init_git();
1297
+ init_monorepo();
1298
+ CONFIG_PATH2 = (0, import_path7.join)((0, import_os4.homedir)(), CONFIG_DIR);
1299
+ PROVIDER_URLS = {
1300
+ ollama: "http://localhost:11434",
1301
+ lmstudio: "http://localhost:1234/v1",
1302
+ openrouter: "https://openrouter.ai/api/v1",
1303
+ custom: "",
1304
+ cloudflare: ""
1305
+ };
1306
+ DEFAULT_MODELS = {
1307
+ ollama: "codellama",
1308
+ lmstudio: "default",
1309
+ openrouter: "google/gemini-flash-1.5-8b",
1310
+ custom: "",
1311
+ cloudflare: "@cf/qwen/qwen2.5-coder-32b-instruct"
1312
+ };
1313
+ }
1314
+ });
1315
+
1316
+ // src/index.ts
1317
+ init_config();
1318
+ init_api();
1319
+ init_git();
1320
+ init_monorepo();
1321
+ var HELP = `QuikCommit - AI-powered conventional commit messages
1322
+
1323
+ Usage:
1324
+ qc Generate commit message and commit (default)
1325
+ qc --message-only Generate message only, print to stdout
1326
+ qc --push Commit and push to origin
1327
+ qc pr Generate PR description from branch commits
1328
+ qc changelog Generate changelog from commits since last tag
1329
+ qc init Install prepare-commit-msg hook for auto-generation
1330
+ qc login Sign in via browser
1331
+ qc logout Clear local credentials
1332
+ qc status Show auth, plan, usage
1333
+ qc team Team management (info, rules, invite)
1334
+
1335
+ Options:
1336
+ -h, --help Show this help
1337
+ -m, --message-only Generate message only
1338
+ -p, --push Commit and push after generating
1339
+ --api-key <key> Use this API key (overrides credentials file)
1340
+ --base <branch> Base branch for qc pr (default: main)
1341
+ --create Create PR with gh CLI after qc pr
1342
+ --from <ref> Start ref for qc changelog (default: latest tag)
1343
+ --to <ref> End ref for qc changelog (default: HEAD)
1344
+ --write Prepend changelog to CHANGELOG.md
1345
+ --version <ver> Version label for changelog header (default: derived from --to or "<from>-next")
1346
+ --uninstall Remove QuikCommit hook (qc init --uninstall)
1347
+ --model <id> Use specific model (e.g. qwen25-coder-32b, llama-3.3-70b)
1348
+
1349
+ Commands:
1350
+ qc config Show current config
1351
+ qc config set <k> <v> Set config (model, api_url)
1352
+ qc config reset Reset to defaults
1353
+ qc upgrade Open billing page in browser
1354
+ `;
1355
+ function parseArgs(args) {
1356
+ let command = "commit";
1357
+ let messageOnly = false;
1358
+ let push = false;
1359
+ let apiKey;
1360
+ let model;
1361
+ let local = false;
1362
+ let base;
1363
+ let create = false;
1364
+ let from;
1365
+ let to;
1366
+ let write = false;
1367
+ let version;
1368
+ let uninstall = false;
1369
+ let hookMode = false;
1370
+ for (let i = 0; i < args.length; i++) {
1371
+ const arg = args[i];
1372
+ if (arg === "-h" || arg === "--help") {
1373
+ command = "help";
1374
+ } else if (arg === "-m" || arg === "--message-only") {
1375
+ messageOnly = true;
1376
+ } else if (arg === "-p" || arg === "--push") {
1377
+ push = true;
1378
+ } else if (arg === "--api-key" && i + 1 < args.length) {
1379
+ apiKey = args[++i];
1380
+ } else if (arg === "--base" && i + 1 < args.length) {
1381
+ base = args[++i];
1382
+ } else if (arg === "--create") {
1383
+ create = true;
1384
+ } else if (arg === "--from" && i + 1 < args.length) {
1385
+ from = args[++i];
1386
+ } else if (arg === "--to" && i + 1 < args.length) {
1387
+ to = args[++i];
1388
+ } else if (arg === "--write") {
1389
+ write = true;
1390
+ } else if (arg === "--version" && i + 1 < args.length) {
1391
+ version = args[++i];
1392
+ } else if (arg === "--uninstall") {
1393
+ uninstall = true;
1394
+ } else if (arg === "--hook-mode") {
1395
+ hookMode = true;
1396
+ } else if (arg === "login") {
1397
+ command = "login";
1398
+ } else if (arg === "logout") {
1399
+ command = "logout";
1400
+ } else if (arg === "status") {
1401
+ command = "status";
1402
+ } else if (arg === "pr") {
1403
+ command = "pr";
1404
+ } else if (arg === "changelog") {
1405
+ command = "changelog";
1406
+ } else if (arg === "init") {
1407
+ command = "init";
1408
+ } else if (arg === "team") {
1409
+ command = "team";
1410
+ } else if (arg === "config") {
1411
+ command = "config";
1412
+ } else if (arg === "upgrade") {
1413
+ command = "upgrade";
1414
+ } else if (arg === "--model" && i + 1 < args.length) {
1415
+ model = args[++i];
1416
+ } else if (arg === "--local" || arg === "--use-ollama" || arg === "--use-lmstudio" || arg === "--use-openrouter" || arg === "--use-cloudflare") {
1417
+ local = true;
1418
+ if (arg === "--use-ollama") {
1419
+ saveConfig({ ...getConfig(), provider: "ollama", apiUrl: "http://localhost:11434", model: "codellama" });
1420
+ } else if (arg === "--use-lmstudio") {
1421
+ saveConfig({ ...getConfig(), provider: "lmstudio", apiUrl: "http://localhost:1234/v1", model: "default" });
1422
+ } else if (arg === "--use-openrouter") {
1423
+ saveConfig({ ...getConfig(), provider: "openrouter", apiUrl: "https://openrouter.ai/api/v1", model: "google/gemini-flash-1.5-8b" });
1424
+ } else if (arg === "--use-cloudflare") {
1425
+ saveConfig({
1426
+ ...getConfig(),
1427
+ provider: "cloudflare",
1428
+ apiUrl: "https://YOUR-WORKER.workers.dev",
1429
+ model: "@cf/qwen/qwen2.5-coder-32b-instruct"
1430
+ });
1431
+ console.error(
1432
+ "[qc] Cloudflare provider set. Run: qc config set api_url https://your-worker.workers.dev"
1433
+ );
1434
+ }
1435
+ }
1436
+ }
1437
+ return { command, messageOnly, push, apiKey, base, create, from, to, write, version, uninstall, hookMode, model, local };
1438
+ }
1439
+ async function runCommit(messageOnly, push, apiKeyFlag, hookMode = false, modelFlag) {
1440
+ const log = hookMode ? () => {
1441
+ } : (msg) => console.error(msg);
1442
+ if (!isGitRepo()) {
1443
+ log("Error: Not a git repository.");
1444
+ process.exit(1);
1445
+ }
1446
+ if (!hasStagedChanges()) {
1447
+ log("Error: No staged changes. Stage files with `git add` first.");
1448
+ process.exit(1);
1449
+ }
1450
+ const apiKey = apiKeyFlag ?? getApiKey();
1451
+ if (!apiKey) {
1452
+ log("Error: Not authenticated. Run `qc login` first.");
1453
+ process.exit(1);
1454
+ }
1455
+ const config2 = getConfig();
1456
+ const model = modelFlag ?? config2.model;
1457
+ const excludes = config2.excludes ?? [];
1458
+ const diff = getStagedDiff(excludes);
1459
+ const changes = getStagedFiles();
1460
+ let rules = config2.rules ?? {};
1461
+ const workspace = detectWorkspace();
1462
+ let monorepoScopes;
1463
+ if (workspace) {
1464
+ const stagedFiles = changes.trim().split("\n").filter(Boolean);
1465
+ const scope = autoDetectScope(stagedFiles, workspace);
1466
+ if (scope) {
1467
+ monorepoScopes = scope.split(",").map((s) => s.trim());
1468
+ rules = { ...rules, scopes: monorepoScopes };
1469
+ }
1470
+ }
1471
+ const client = new ApiClient({ apiKey });
1472
+ try {
1473
+ const teamRules = await client.getTeamRules();
1474
+ if (teamRules && Object.keys(teamRules).length > 0) {
1475
+ log("[qc] Using team rules from org");
1476
+ rules = { ...rules, ...teamRules };
1477
+ if (monorepoScopes && teamRules.scopes && teamRules.scopes.length > 0) {
1478
+ const allowed = new Set(teamRules.scopes);
1479
+ const intersected = monorepoScopes.filter((s) => allowed.has(s));
1480
+ if (intersected.length > 0) rules = { ...rules, scopes: intersected };
1481
+ }
1482
+ }
1483
+ } catch {
1484
+ }
1485
+ const { message } = await client.generateCommit(diff, changes, rules, model);
1486
+ if (messageOnly) {
1487
+ console.log(message);
1488
+ return;
1489
+ }
1490
+ gitCommit(message);
1491
+ if (push) {
1492
+ gitPush();
1493
+ }
1494
+ }
1495
+ async function main() {
1496
+ const argv = process.argv.slice(2);
1497
+ const values = parseArgs(argv);
1498
+ const { command, messageOnly, push, apiKey } = values;
1499
+ if (command === "help") {
1500
+ console.log(HELP);
1501
+ return;
1502
+ }
1503
+ if (command === "login") {
1504
+ const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
1505
+ await runLogin2();
1506
+ return;
1507
+ }
1508
+ if (command === "logout") {
1509
+ const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
1510
+ runLogout2();
1511
+ return;
1512
+ }
1513
+ if (command === "status") {
1514
+ const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
1515
+ await runStatus2(apiKey);
1516
+ return;
1517
+ }
1518
+ if (command === "pr") {
1519
+ const { pr: pr2 } = await Promise.resolve().then(() => (init_pr(), pr_exports));
1520
+ await pr2({
1521
+ base: values.base,
1522
+ create: values.create,
1523
+ model: values.model ?? getConfig().model
1524
+ });
1525
+ return;
1526
+ }
1527
+ if (command === "changelog") {
1528
+ const { changelog: changelog2 } = await Promise.resolve().then(() => (init_changelog(), changelog_exports));
1529
+ await changelog2({
1530
+ from: values.from,
1531
+ to: values.to,
1532
+ write: values.write,
1533
+ version: values.version,
1534
+ model: values.model ?? getConfig().model
1535
+ });
1536
+ return;
1537
+ }
1538
+ if (command === "init") {
1539
+ const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1540
+ await init2({ uninstall: values.uninstall });
1541
+ return;
1542
+ }
1543
+ if (command === "team") {
1544
+ const { team: team2 } = await Promise.resolve().then(() => (init_team(), team_exports));
1545
+ const positionals = argv.filter((a) => !a.startsWith("-") && a !== "team");
1546
+ await team2(positionals[0], positionals.slice(1));
1547
+ return;
1548
+ }
1549
+ if (command === "config") {
1550
+ const { config: config2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
1551
+ const positionals = argv.filter((a) => !a.startsWith("-") && a !== "config");
1552
+ await config2(positionals);
1553
+ return;
1554
+ }
1555
+ if (command === "upgrade") {
1556
+ const { upgrade: upgrade2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
1557
+ await upgrade2();
1558
+ return;
1559
+ }
1560
+ if (values.local) {
1561
+ const { runLocalCommit: runLocalCommit2 } = await Promise.resolve().then(() => (init_local(), local_exports));
1562
+ await runLocalCommit2(messageOnly, push, values.model);
1563
+ return;
1564
+ }
1565
+ const apiKeyToUse = apiKey ?? getApiKey();
1566
+ if (!apiKeyToUse) {
1567
+ const { getLocalProviderConfig: getLocalProviderConfig2 } = await Promise.resolve().then(() => (init_local(), local_exports));
1568
+ if (getLocalProviderConfig2()) {
1569
+ const { runLocalCommit: runLocalCommit2 } = await Promise.resolve().then(() => (init_local(), local_exports));
1570
+ await runLocalCommit2(messageOnly, push, values.model);
1571
+ return;
1572
+ }
1573
+ }
1574
+ await runCommit(messageOnly, push, apiKey, values.hookMode, values.model);
1575
+ }
1576
+ main().catch((err) => {
1577
+ const args = process.argv.slice(2);
1578
+ const hookMode = args.includes("--hook-mode");
1579
+ if (!hookMode) {
1580
+ console.error(err instanceof Error ? err.message : String(err));
1581
+ }
1582
+ process.exit(1);
1583
+ });