@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.
- package/dist/index.js +1583 -0
- 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
|
+
});
|